From e91c53cb07ae080451d973baf07fde53b6ef2bb5 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 29 Jan 2026 16:01:48 -0500 Subject: [PATCH 1/4] updated check_dataset return object with print/knit_print --- NAMESPACE | 2 + R/dataset.R | 66 ++++++++++++++ R/extendr-wrappers.R | 9 +- _starlightr.toml | 105 ++++++++++------------ man/check_dataset.Rd | 10 +-- man/knit_print.hyperion_nonmem_dataset.Rd | 19 ++++ man/print.hyperion_nonmem_dataset.Rd | 19 ++++ src/rust/nonmem/src/model/mod.rs | 26 +++--- vignettes/hyperion_model.Rmd | 6 +- 9 files changed, 180 insertions(+), 82 deletions(-) create mode 100644 R/dataset.R create mode 100644 man/knit_print.hyperion_nonmem_dataset.Rd create mode 100644 man/print.hyperion_nonmem_dataset.Rd diff --git a/NAMESPACE b/NAMESPACE index 1ee0aa2..30883d9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,11 +3,13 @@ S3method(base::`$`, hyperion_nonmem_model) S3method(base::`[[`, hyperion_nonmem_model) S3method(base::names, hyperion_nonmem_model) +S3method(base::print, hyperion_nonmem_dataset) S3method(base::print, hyperion_nonmem_model) S3method(base::print, hyperion_nonmem_summary) S3method(base::print, hyperion_nonmem_tree) S3method(base::print, parameter_audit) S3method(base::summary, hyperion_nonmem_model) +S3method(knitr::knit_print,hyperion_nonmem_dataset) S3method(knitr::knit_print,hyperion_nonmem_model) S3method(knitr::knit_print,hyperion_nonmem_summary) S3method(knitr::knit_print,hyperion_nonmem_tree) diff --git a/R/dataset.R b/R/dataset.R new file mode 100644 index 0000000..d780bfa --- /dev/null +++ b/R/dataset.R @@ -0,0 +1,66 @@ +#' Make path relative to pharos.toml directory +#' +#' @param path Absolute path to make relative +#' @return Path relative to pharos.toml directory, or original path if not possible +#' @noRd +make_pharos_relative_path <- function(path) { + tryCatch( + { + config_path <- find_pharos_config_file() + if (grepl("No pharos.toml", config_path)) { + return(path) + } + config_dir <- dirname(config_path) + + # Check if path starts with config_dir + if (startsWith(path, config_dir)) { + rel_path <- substring(path, nchar(config_dir) + 2) # +2 for trailing / + return(rel_path) + } + path + }, + error = function(e) path + ) +} + +#' Print method for hyperion_nonmem_dataset objects +#' +#' @param x A hyperion_nonmem_dataset object +#' @param ... Additional arguments (ignored) +#' @return Invisible copy of x +#' @rawNamespace S3method(base::print, hyperion_nonmem_dataset) +print.hyperion_nonmem_dataset <- function(x, ...) { + rel_path <- make_pharos_relative_path(x$canonical_path) + + cli::cli_h1("Dataset Check") + cli::cli_text("{.strong Path:} {rel_path}") + cli::cli_text("{.strong Hash:} {x$blake3_hash}") + + invisible(x) +} + +#' Knit print method for hyperion_nonmem_dataset objects +#' +#' @param x A hyperion_nonmem_dataset object +#' @param ... Additional arguments (ignored) +#' @return HTML output for rendered documents +#' @exportS3Method knitr::knit_print +knit_print.hyperion_nonmem_dataset <- function(x, ...) { + rel_path <- make_pharos_relative_path(x$canonical_path) + + df <- data.frame( + Field = c("Path", "Hash"), + Value = c(rel_path, x$blake3_hash), + stringsAsFactors = FALSE + ) + + tbl <- knitr::kable(df, format = "html", col.names = NULL, escape = FALSE) + + output <- c( + "Dataset Check", + "", + tbl + ) + + knitr::asis_output(paste(output, collapse = "\n")) +} diff --git a/R/extendr-wrappers.R b/R/extendr-wrappers.R index 1f2fae6..1a72a4d 100644 --- a/R/extendr-wrappers.R +++ b/R/extendr-wrappers.R @@ -41,17 +41,16 @@ read_model <- function(path) .Call(wrap__read_model, path) #' Checks model dataset #' -#' @param model list of model object from `read_model` -#' @param model_dir directory of model output //TODO check this +#' @param model hyperion_nonmem_model object from `read_model` #' -#' @return nothing //todo maybe a true/false? +#' @return Dataset check results #' @export #' #' @examples \dontrun{ #' model <- read_model("model/nonmem/run001.mod") -#' model |> check_dataset("model/nonmem/run001") +#' model |> check_dataset() #' } -check_dataset <- function(model, model_dir) .Call(wrap__check_dataset, model, model_dir) +check_dataset <- function(model) .Call(wrap__check_dataset, model) #' Gets model object from lst file (internal) #' diff --git a/_starlightr.toml b/_starlightr.toml index 6ff888b..e8cd274 100644 --- a/_starlightr.toml +++ b/_starlightr.toml @@ -7,7 +7,10 @@ dir = "../Docs/hyperion-docs" [versions] enabled = true -list = [{ tag = "0.1.1", label = "v0.1.1", default = true }] +list = [ + { tag = "0.2.0", label = "v0.2.0", default = true }, + { tag = "0.1.1", label = "v0.1.1" }, +] [home] hero = { actions = [ @@ -16,7 +19,8 @@ hero = { actions = [ ] } cards = [ { icon = "document", title = "Model Interaction", description = "hyperion has several functions for interacting with NONMEM models", link = "./reference/read_model/" }, - { icon = "seti:csv", title = "Parameter Tables", description = "hyperion can create model parameter tables", link = "./articles/parameter-tables/" }, + { icon = "open-book", title = "Hyperion Reference", description = "See the full hyperion reference for all functionality", link = "./reference/hyperion-package/" }, + ] [sidebar] @@ -33,88 +37,73 @@ articles = [ { label = "NONMEM Model Interactions", contents = [ "hyperion_model", ] }, - { label = "Parameter Tables", contents = [ - "parameter-tables", - ] }, ] reference = [ - { label = "Models", contents = [ + "hyperion-package", + { label = "Model I/O", contents = [ "read_model", + "copy_model", "check_model", "check_dataset", - "copy_model", - { slug = "summary.hyperion_nonmem_model", label = "summary" }, - "get_run_info", - "get_parameters", - "get_model_parameter_info", - "get_parameter_names", - "get_model_lineage", - "are_models_in_lineage", - "get_model_ancestors", - "get_model_descendants", - "read_model_from_lst", - ] }, - { label = "Model Metadata", contents = [ - "set_metadata_file", - "update_metadata_file", - ] }, - { label = "Parameter Computations", contents = [ - "compute_ci", - "compute_cv", - "compute_rse", - "transform_value", ] }, - { label = "Parameter Tables", contents = [ - "Tablespec", - "apply_table_spec", - "get_table_spec", - "add_summary_info", - "make_parameter_table", - "compare_with", - "make_comparison_table", - "add_model_lineage", - "filter_rules", - "section_rules", - ] }, - { label = "Model Lineage Tables", contents = [ - "SummarySpec", - "apply_summary_spec", - "get_summary_spec", - "make_summary_table", - "summary_filter_rules", + { label = "Model Summaries", contents = [ + "summary.hyperion_nonmem_model", + "get_run_info", ] }, - { label = "Output Files", contents = [ - "get_eta_shrinkage", - "get_eps_shrinkage", - "get_gradients", + { label = "Parameter Extraction", contents = [ + "get_parameters", "get_final_estimates", "read_ext_file", + "get_gradients", + "get_eta_shrinkage", + "get_eps_shrinkage", ] }, - { label = "Comment Parsing", contents = [ + { label = "Parameter Metadata", contents = [ + "get_model_parameter_info", "ThetaComment", - "get_theta_names", "OmegaComment", - "get_eta_labels", "SigmaComment", "ModelComments", - "get_model_parameter_names", "get_comment", - "get_comment_type", - "audit_parameter_info", + "get_parameter_names", "get_parameter_transform", "get_parameter_unit", + "get_theta_names", + "get_eta_labels", + "update_param_info", + "audit_parameter_info", ] }, - { label = "Lookup & Rules", contents = [ + { label = "Lookup Files", contents = [ "apply_lookup", "apply_lookup_defaults", "add_parameter_to_lookup", "remove_parameter_from_lookup", "list_lookup_parameters", ] }, - { label = "Configuration & Submission", contents = [ + { label = "Transform Calculations", contents = [ + "compute_cv", + "compute_rse", + "compute_ci", + "transform_value", + ] }, + { label = "Model Lineage", contents = [ + "get_model_lineage", + "get_model_ancestors", + "get_model_descendants", + "are_models_in_lineage", + ] }, + { label = "Configuration", contents = [ "init", "get_pharos_config", - "submit_model_to_sge", + "get_comment_type", + "use_type1_comments", + ] }, + { label = "Metadata Files", contents = [ + "set_metadata_file", + "update_metadata_file", + ] }, + { label = "Job Submission", contents = [ "submit_model_to_slurm", + "submit_model_to_sge", ] }, ] diff --git a/man/check_dataset.Rd b/man/check_dataset.Rd index 2ae678d..a48b502 100644 --- a/man/check_dataset.Rd +++ b/man/check_dataset.Rd @@ -4,15 +4,13 @@ \alias{check_dataset} \title{Checks model dataset} \usage{ -check_dataset(model, model_dir) +check_dataset(model) } \arguments{ -\item{model}{list of model object from \code{read_model}} - -\item{model_dir}{directory of model output //TODO check this} +\item{model}{hyperion_nonmem_model object from \code{read_model}} } \value{ -nothing //todo maybe a true/false? +Dataset check results } \description{ Checks model dataset @@ -20,6 +18,6 @@ Checks model dataset \examples{ \dontrun{ model <- read_model("model/nonmem/run001.mod") -model |> check_dataset("model/nonmem/run001") +model |> check_dataset() } } diff --git a/man/knit_print.hyperion_nonmem_dataset.Rd b/man/knit_print.hyperion_nonmem_dataset.Rd new file mode 100644 index 0000000..10384e7 --- /dev/null +++ b/man/knit_print.hyperion_nonmem_dataset.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dataset.R +\name{knit_print.hyperion_nonmem_dataset} +\alias{knit_print.hyperion_nonmem_dataset} +\title{Knit print method for hyperion_nonmem_dataset objects} +\usage{ +\method{knit_print}{hyperion_nonmem_dataset}(x, ...) +} +\arguments{ +\item{x}{A hyperion_nonmem_dataset object} + +\item{...}{Additional arguments (ignored)} +} +\value{ +HTML output for rendered documents +} +\description{ +Knit print method for hyperion_nonmem_dataset objects +} diff --git a/man/print.hyperion_nonmem_dataset.Rd b/man/print.hyperion_nonmem_dataset.Rd new file mode 100644 index 0000000..c7823eb --- /dev/null +++ b/man/print.hyperion_nonmem_dataset.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dataset.R +\name{print.hyperion_nonmem_dataset} +\alias{print.hyperion_nonmem_dataset} +\title{Print method for hyperion_nonmem_dataset objects} +\usage{ +\method{print}{hyperion_nonmem_dataset}(x, ...) +} +\arguments{ +\item{x}{A hyperion_nonmem_dataset object} + +\item{...}{Additional arguments (ignored)} +} +\value{ +Invisible copy of x +} +\description{ +Print method for hyperion_nonmem_dataset objects +} diff --git a/src/rust/nonmem/src/model/mod.rs b/src/rust/nonmem/src/model/mod.rs index 45bba3e..2232360 100644 --- a/src/rust/nonmem/src/model/mod.rs +++ b/src/rust/nonmem/src/model/mod.rs @@ -4,7 +4,7 @@ use extendr_api::prelude::*; use extendr_api::serializer::to_robj; use fs_err as fs; -use std::path::{Path, PathBuf}; +use std::path::Path; //pharos nonmem crate use nonmem::Model; @@ -13,8 +13,9 @@ use nonmem::output_files::lst; use crate::model::run_status::determine_run_status; use crate::utils::{ find_output_file, get_comment_type, get_model_source_path, resolve_input_model_path, + resolve_model_input_path_from_robj, }; -use hyperion_core::ResultExt; +use hyperion_core::{OptionExt, ResultExt}; pub mod check; pub mod copy; @@ -126,24 +127,29 @@ pub fn read_model_from_lst(path: &str) -> Result { /// Checks model dataset /// -/// @param model list of model object from `read_model` -/// @param model_dir directory of model output //TODO check this +/// @param model hyperion_nonmem_model object from `read_model` /// -/// @return nothing //todo maybe a true/false? +/// @return Dataset check results /// @export /// /// @examples \dontrun{ /// model <- read_model("model/nonmem/run001.mod") -/// model |> check_dataset("model/nonmem/run001") +/// model |> check_dataset() /// } #[extendr] -pub fn check_dataset(model: Robj, model_dir: &str) -> Result { +pub fn check_dataset(model: Robj) -> Result { + let model_path = resolve_model_input_path_from_robj(&model)?; + let model_dir = model_path + .parent() + .ok_or_extendr_err("Could not determine model directory")?; + let model = robj_to_model(&model)?; + let dataset = model.check_dataset(model_dir).map_to_extendr_err("")?; - let model_dir = PathBuf::from(model_dir); - let dataset = model.check_dataset(&model_dir).map_to_extendr_err("")?; + let mut robj = to_robj(&dataset).map_to_extendr_err("Failed to serialize to Robj")?; - let robj = to_robj(&dataset).map_to_extendr_err("Failed to serialize to Robj")?; + robj.set_class(["hyperion_nonmem_dataset"]) + .map_to_extendr_err("Failed to set class")?; Ok(robj) } diff --git a/vignettes/hyperion_model.Rmd b/vignettes/hyperion_model.Rmd index 9ea558f..bea6582 100644 --- a/vignettes/hyperion_model.Rmd +++ b/vignettes/hyperion_model.Rmd @@ -44,13 +44,13 @@ attributes(mod) |> names() ```{r} read_model(file.path(test_data_dir, "models", "onecmt", "run001.mod")) |> - check_dataset(file.path(test_data_dir, "models", "onecmt")) + check_dataset() read_model(file.path(test_data_dir, "models", "onecmt", "run002.mod")) |> - check_dataset(file.path(test_data_dir, "models", "onecmt")) + check_dataset() read_model(file.path(test_data_dir, "models", "onecmt", "run003.mod")) |> - check_dataset(file.path(test_data_dir, "models", "onecmt")) + check_dataset() ``` From 31347f888715435f6e4d8965b68cbb6468d3ad37 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 29 Jan 2026 16:25:42 -0500 Subject: [PATCH 2/4] fixed check_dataset for lst models --- src/rust/nonmem/src/model/mod.rs | 9 +++++++-- src/rust/nonmem/src/utils.rs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/rust/nonmem/src/model/mod.rs b/src/rust/nonmem/src/model/mod.rs index 2232360..d0bae47 100644 --- a/src/rust/nonmem/src/model/mod.rs +++ b/src/rust/nonmem/src/model/mod.rs @@ -13,7 +13,7 @@ use nonmem::output_files::lst; use crate::model::run_status::determine_run_status; use crate::utils::{ find_output_file, get_comment_type, get_model_source_path, resolve_input_model_path, - resolve_model_input_path_from_robj, + resolve_model_source_path, }; use hyperion_core::{OptionExt, ResultExt}; @@ -138,7 +138,12 @@ pub fn read_model_from_lst(path: &str) -> Result { /// } #[extendr] pub fn check_dataset(model: Robj) -> Result { - let model_path = resolve_model_input_path_from_robj(&model)?; + let source = model + .get_attrib("model_source") + .ok_or_extendr_err("Model object is missing model_source attribute")? + .as_str() + .ok_or_extendr_err("model_source attribute must be a character")?; + let model_path = resolve_model_source_path(source)?; let model_dir = model_path .parent() .ok_or_extendr_err("Could not determine model directory")?; diff --git a/src/rust/nonmem/src/utils.rs b/src/rust/nonmem/src/utils.rs index 33967f5..610b4d8 100644 --- a/src/rust/nonmem/src/utils.rs +++ b/src/rust/nonmem/src/utils.rs @@ -161,8 +161,8 @@ pub fn get_model_source_path(path: impl AsRef) -> Result { } /// Resolve a model source string into an absolute or config-relative path. -pub fn resolve_model_source_path(source: &str) -> Result { - let source_path = Path::new(source); +pub fn resolve_model_source_path(source: impl AsRef) -> Result { + let source_path = source.as_ref(); if source_path.is_absolute() { return Ok(source_path.to_path_buf()); } From b9f30d76b82568ce6dbf0f9ed0c4125f6f3ca780 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 29 Jan 2026 16:59:28 -0500 Subject: [PATCH 3/4] added getters for model info, consolidated path functions --- NAMESPACE | 3 + R/comments-parsing.R | 6 +- R/dataset.R | 29 +----- R/extendr-wrappers.R | 16 +++- R/hyperion-package.R | 3 + R/model-methods.R | 59 +++++++++++++ _starlightr.toml | 3 + man/get_data_path.Rd | 23 +++++ man/get_model_dir.Rd | 23 +++++ man/get_model_name.Rd | 23 +++++ man/hyperion-package.Rd | 3 + src/rust/nonmem/src/model/check.rs | 4 +- src/rust/nonmem/src/model/copy.rs | 4 +- src/rust/nonmem/src/model/lineage.rs | 4 +- src/rust/nonmem/src/model/metadata.rs | 6 +- src/rust/nonmem/src/model/mod.rs | 10 +-- src/rust/nonmem/src/model/run_status.rs | 4 +- src/rust/nonmem/src/model/summary.rs | 8 +- src/rust/nonmem/src/utils.rs | 113 ++++++++++++++++-------- src/rust/scheduler/src/lib.rs | 6 +- 20 files changed, 256 insertions(+), 94 deletions(-) create mode 100644 man/get_data_path.Rd create mode 100644 man/get_model_dir.Rd create mode 100644 man/get_model_name.Rd diff --git a/NAMESPACE b/NAMESPACE index 30883d9..5a5282f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -35,6 +35,7 @@ export(format_hyperion_sigfig_string) export(format_omega_display_name) export(get_comment) export(get_comment_type) +export(get_data_path) export(get_eps_shrinkage) export(get_eta_labels) export(get_eta_shrinkage) @@ -42,7 +43,9 @@ export(get_final_estimates) export(get_gradients) export(get_model_ancestors) export(get_model_descendants) +export(get_model_dir) export(get_model_lineage) +export(get_model_name) export(get_model_parameter_info) export(get_model_summary) export(get_parameter_names) diff --git a/R/comments-parsing.R b/R/comments-parsing.R index e07e886..c6afa0e 100644 --- a/R/comments-parsing.R +++ b/R/comments-parsing.R @@ -69,7 +69,7 @@ read_model_from_lst_dir <- function(dir_path) { #' Derive output directory from model path and read from .lst file #' @noRd read_model_from_lst_path <- function(mod_path) { - mod_path <- resolve_model_source_path(mod_path) + mod_path <- from_config_relative(mod_path) # Derive output directory: run001.mod -> run001/ base_name <- tools::file_path_sans_ext(basename(mod_path)) parent_dir <- dirname(mod_path) @@ -129,7 +129,7 @@ get_model_parameter_info <- function(mod, lookup_path = NULL) { } else if (inherits(mod, "hyperion_nonmem_model")) { mod_path <- attr(mod, "model_source") %||% "unknown" if (!identical(mod_path, "unknown")) { - mod_path <- resolve_model_source_path(mod_path) + mod_path <- from_config_relative(mod_path) } # If model was read from .mod/.ctl file, find and read from .lst instead if (!grepl("\\.lst$", mod_path, ignore.case = TRUE)) { @@ -153,7 +153,7 @@ get_model_parameter_info <- function(mod, lookup_path = NULL) { mod_path <- attr(mod, "model_source") %||% "unknown" if (!identical(mod_path, "unknown")) { - mod_path <- resolve_model_source_path(mod_path) + mod_path <- from_config_relative(mod_path) } param_names <- get_model_parameter_names(mod) diff --git a/R/dataset.R b/R/dataset.R index d780bfa..fc45fe9 100644 --- a/R/dataset.R +++ b/R/dataset.R @@ -1,28 +1,3 @@ -#' Make path relative to pharos.toml directory -#' -#' @param path Absolute path to make relative -#' @return Path relative to pharos.toml directory, or original path if not possible -#' @noRd -make_pharos_relative_path <- function(path) { - tryCatch( - { - config_path <- find_pharos_config_file() - if (grepl("No pharos.toml", config_path)) { - return(path) - } - config_dir <- dirname(config_path) - - # Check if path starts with config_dir - if (startsWith(path, config_dir)) { - rel_path <- substring(path, nchar(config_dir) + 2) # +2 for trailing / - return(rel_path) - } - path - }, - error = function(e) path - ) -} - #' Print method for hyperion_nonmem_dataset objects #' #' @param x A hyperion_nonmem_dataset object @@ -30,7 +5,7 @@ make_pharos_relative_path <- function(path) { #' @return Invisible copy of x #' @rawNamespace S3method(base::print, hyperion_nonmem_dataset) print.hyperion_nonmem_dataset <- function(x, ...) { - rel_path <- make_pharos_relative_path(x$canonical_path) + rel_path <- to_config_relative(x$canonical_path) cli::cli_h1("Dataset Check") cli::cli_text("{.strong Path:} {rel_path}") @@ -46,7 +21,7 @@ print.hyperion_nonmem_dataset <- function(x, ...) { #' @return HTML output for rendered documents #' @exportS3Method knitr::knit_print knit_print.hyperion_nonmem_dataset <- function(x, ...) { - rel_path <- make_pharos_relative_path(x$canonical_path) + rel_path <- to_config_relative(x$canonical_path) df <- data.frame( Field = c("Path", "Hash"), diff --git a/R/extendr-wrappers.R b/R/extendr-wrappers.R index 1a72a4d..e2751a9 100644 --- a/R/extendr-wrappers.R +++ b/R/extendr-wrappers.R @@ -420,15 +420,25 @@ get_pharos_config <- function() .Call(wrap__get_pharos_config) #' } get_comment_type <- function() .Call(wrap__get_comment_type_wrap) +#' Validate and resolve a model path (.mod or .ctl). +#' +#' @keywords internal +#' @noRd +validate_model_path <- function(path) .Call(wrap__validate_model_path_wrap, path) + +#' Convert a config-relative path to absolute. +#' #' @keywords internal #' @noRd -resolve_input_model_path <- function(path) .Call(wrap__resolve_input_model_path_wrap, path) +from_config_relative <- function(path) .Call(wrap__from_config_relative_wrap, path) -#' Resolve a model_source string into an absolute or config-relative path. +#' Convert an absolute path to be relative to the pharos config directory. #' +#' @param path Absolute path to make relative. +#' @return Path relative to pharos.toml directory, or original path if not under config dir. #' @keywords internal #' @noRd -resolve_model_source_path <- function(path) .Call(wrap__resolve_model_source_path_wrap, path) +to_config_relative <- function(path) .Call(wrap__to_config_relative_wrap, path) #' Submits a NONMEM model to SLURM for execution #' diff --git a/R/hyperion-package.R b/R/hyperion-package.R index cf15239..b910ea4 100644 --- a/R/hyperion-package.R +++ b/R/hyperion-package.R @@ -12,6 +12,9 @@ #' \item [copy_model()] - Copy a model to a new file with optional parameter updates #' \item [check_model()] - Validate model syntax #' \item [check_dataset()] - Validate model dataset +#' \item [get_model_name()] - Get the model name (filename without extension) +#' \item [get_model_dir()] - Get the model directory path +#' \item [get_data_path()] - Get the dataset path from the model #' } #' #' @section Model Summaries: diff --git a/R/model-methods.R b/R/model-methods.R index c775674..395110c 100644 --- a/R/model-methods.R +++ b/R/model-methods.R @@ -684,3 +684,62 @@ knit_print.hyperion_nonmem_model <- function(x, ...) { # Return as HTML knitr::asis_output(paste(output, collapse = "\n")) } + +#' Get model name +#' +#' Extracts the model name from a hyperion_nonmem_model object. +#' +#' @param model A hyperion_nonmem_model object +#' @return Character string with the model name (filename without extension) +#' @export +#' +#' @examples +#' \dontrun{ +#' mod <- read_model("models/run001.mod") +#' get_model_name(mod) # "run001" +#' } +get_model_name <- function(model) { + model_source <- attr(model, "model_source") + if (is.null(model_source)) { + stop("Model object is missing model_source attribute") + } + tools::file_path_sans_ext(basename(model_source)) +} + +#' Get model directory +#' +#' Extracts the directory path from a hyperion_nonmem_model object. +#' +#' @param model A hyperion_nonmem_model object +#' @return Character string with the model directory path (relative to pharos.toml) +#' @export +#' +#' @examples +#' \dontrun{ +#' mod <- read_model("models/run001.mod") +#' get_model_dir(mod) # "models" +#' } +get_model_dir <- function(model) { + model_source <- attr(model, "model_source") + if (is.null(model_source)) { + stop("Model object is missing model_source attribute") + } + dirname(model_source) +} + +#' Get data path +#' +#' Extracts the dataset path from a hyperion_nonmem_model object. +#' +#' @param model A hyperion_nonmem_model object +#' @return Character string with the data path, or NULL if not found +#' @export +#' +#' @examples +#' \dontrun{ +#' mod <- read_model("models/run001.mod") +#' get_data_path(mod) # "../data/derived/pk_data.csv" +#' } +get_data_path <- function(model) { + model$data$path +} diff --git a/_starlightr.toml b/_starlightr.toml index e8cd274..bcdbe0b 100644 --- a/_starlightr.toml +++ b/_starlightr.toml @@ -45,6 +45,9 @@ reference = [ "copy_model", "check_model", "check_dataset", + "get_model_name", + "get_model_dir", + "get_data_path", ] }, { label = "Model Summaries", contents = [ "summary.hyperion_nonmem_model", diff --git a/man/get_data_path.Rd b/man/get_data_path.Rd new file mode 100644 index 0000000..d75e5af --- /dev/null +++ b/man/get_data_path.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-methods.R +\name{get_data_path} +\alias{get_data_path} +\title{Get data path} +\usage{ +get_data_path(model) +} +\arguments{ +\item{model}{A hyperion_nonmem_model object} +} +\value{ +Character string with the data path, or NULL if not found +} +\description{ +Extracts the dataset path from a hyperion_nonmem_model object. +} +\examples{ +\dontrun{ +mod <- read_model("models/run001.mod") +get_data_path(mod) # "../data/derived/pk_data.csv" +} +} diff --git a/man/get_model_dir.Rd b/man/get_model_dir.Rd new file mode 100644 index 0000000..831758f --- /dev/null +++ b/man/get_model_dir.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-methods.R +\name{get_model_dir} +\alias{get_model_dir} +\title{Get model directory} +\usage{ +get_model_dir(model) +} +\arguments{ +\item{model}{A hyperion_nonmem_model object} +} +\value{ +Character string with the model directory path (relative to pharos.toml) +} +\description{ +Extracts the directory path from a hyperion_nonmem_model object. +} +\examples{ +\dontrun{ +mod <- read_model("models/run001.mod") +get_model_dir(mod) # "models" +} +} diff --git a/man/get_model_name.Rd b/man/get_model_name.Rd new file mode 100644 index 0000000..c9bfb4d --- /dev/null +++ b/man/get_model_name.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-methods.R +\name{get_model_name} +\alias{get_model_name} +\title{Get model name} +\usage{ +get_model_name(model) +} +\arguments{ +\item{model}{A hyperion_nonmem_model object} +} +\value{ +Character string with the model name (filename without extension) +} +\description{ +Extracts the model name from a hyperion_nonmem_model object. +} +\examples{ +\dontrun{ +mod <- read_model("models/run001.mod") +get_model_name(mod) # "run001" +} +} diff --git a/man/hyperion-package.Rd b/man/hyperion-package.Rd index 021e293..08fc543 100644 --- a/man/hyperion-package.Rd +++ b/man/hyperion-package.Rd @@ -18,6 +18,9 @@ Functions for reading, writing, and validating NONMEM models: \item \code{\link[=copy_model]{copy_model()}} - Copy a model to a new file with optional parameter updates \item \code{\link[=check_model]{check_model()}} - Validate model syntax \item \code{\link[=check_dataset]{check_dataset()}} - Validate model dataset +\item \code{\link[=get_model_name]{get_model_name()}} - Get the model name (filename without extension) +\item \code{\link[=get_model_dir]{get_model_dir()}} - Get the model directory path +\item \code{\link[=get_data_path]{get_data_path()}} - Get the dataset path from the model } } diff --git a/src/rust/nonmem/src/model/check.rs b/src/rust/nonmem/src/model/check.rs index f1dd208..9c93657 100644 --- a/src/rust/nonmem/src/model/check.rs +++ b/src/rust/nonmem/src/model/check.rs @@ -5,7 +5,7 @@ use hyperion_core::ResultExt; // pharos config and nonmem crates use nonmem::check_model; -use crate::utils::{load_nonmem_config, resolve_model_or_path}; +use crate::utils::{load_nonmem_config, validated_model_from_robj}; use hyperion_core::extendr_err; /// Checks mod file for nmtran errors @@ -22,7 +22,7 @@ use hyperion_core::extendr_err; /// } #[extendr(r_name = "check_model")] pub fn check_model_wrap(model_path: Robj) -> Result { - let model_path = resolve_model_or_path(model_path)?; + let model_path = validated_model_from_robj(&model_path)?; let (_config_path, nonmem_config) = load_nonmem_config(None).map_to_extendr_err("Failed to create NonmemConfig")?; diff --git a/src/rust/nonmem/src/model/copy.rs b/src/rust/nonmem/src/model/copy.rs index 4a4780e..db8b581 100644 --- a/src/rust/nonmem/src/model/copy.rs +++ b/src/rust/nonmem/src/model/copy.rs @@ -5,7 +5,7 @@ use nonmem::copy::{JitterSpec, ParamType, UpdateType}; use nonmem::{CopyOptions, copy_model}; use std::path::{Path, PathBuf}; -use crate::utils::resolve_model_or_path; +use crate::utils::validated_model_from_robj; use hyperion_core::{OptionExt, ResultExt, extendr_err}; // This should move to Option @@ -180,7 +180,7 @@ pub fn copy_model_wrap( no_metadata, }; - let from_path = resolve_model_or_path(from)?; + let from_path = validated_model_from_robj(&from)?; let original_filename = match from_path.file_name() { Some(filename) => filename.to_string_lossy().to_string(), None => Err(extendr_err!("`from` model file does not have a file name"))?, diff --git a/src/rust/nonmem/src/model/lineage.rs b/src/rust/nonmem/src/model/lineage.rs index a19bbb9..cbaa8f5 100644 --- a/src/rust/nonmem/src/model/lineage.rs +++ b/src/rust/nonmem/src/model/lineage.rs @@ -7,7 +7,7 @@ use std::collections::{HashMap, HashSet}; use nonmem::{LineageTree, ModelMetadata, OutputFileHash, RunEndFile, RunStartFile}; -use crate::utils::{get_model_source_path, path_from_robj}; +use crate::utils::{path_from_robj, to_config_relative}; use hyperion_core::{OptionExt, ResultExt}; /// R-compatible version of RunEndFile with u128 -> f64 conversion @@ -99,7 +99,7 @@ pub fn get_model_lineage(model_dir: Robj) -> Result { .map_to_extendr_err("Pharos failed to create lineage tree")?; // Convert to R-compatible version (u128 -> f64) and attach source directory (relative to pharos.toml) - let source_dir = get_model_source_path(&model_dir)?; + let source_dir = to_config_relative(&model_dir)?; let r_lineage: RLineageTree = RLineageTree::from(lineage).with_source_dir(source_dir); // Serialize R-compatible lineage to Robj diff --git a/src/rust/nonmem/src/model/metadata.rs b/src/rust/nonmem/src/model/metadata.rs index d04e897..2c65724 100644 --- a/src/rust/nonmem/src/model/metadata.rs +++ b/src/rust/nonmem/src/model/metadata.rs @@ -4,7 +4,7 @@ use extendr_api::prelude::*; //pharos nonmem crate use nonmem::update_metadata_file; -use crate::utils::resolve_model_or_path; +use crate::utils::validated_model_from_robj; use hyperion_core::{ResultExt, extendr_err}; /// Creates a metadata file for a NONMEM model @@ -53,7 +53,7 @@ pub fn set_metadata_file( )); }; - let model_path = resolve_model_or_path(model_path)?; + let model_path = validated_model_from_robj(&model_path)?; let tags = tags.unwrap_or_default(); let based_on = based_on.unwrap_or_default(); @@ -87,7 +87,7 @@ pub fn append_to_metadata_file( #[extendr(default = "NULL")] tags: Option>, #[extendr(default = "NULL")] based_on: Option>, ) -> Result<()> { - let path = resolve_model_or_path(model_path)?; + let path = validated_model_from_robj(&model_path)?; let tags = tags.unwrap_or_default(); let based_on = based_on.unwrap_or_default(); diff --git a/src/rust/nonmem/src/model/mod.rs b/src/rust/nonmem/src/model/mod.rs index d0bae47..1b47746 100644 --- a/src/rust/nonmem/src/model/mod.rs +++ b/src/rust/nonmem/src/model/mod.rs @@ -12,8 +12,8 @@ use nonmem::output_files::lst; use crate::model::run_status::determine_run_status; use crate::utils::{ - find_output_file, get_comment_type, get_model_source_path, resolve_input_model_path, - resolve_model_source_path, + find_output_file, from_config_relative, get_comment_type, to_config_relative, + validate_model_path, }; use hyperion_core::{OptionExt, ResultExt}; @@ -55,7 +55,7 @@ fn add_filename_attr(model_robj: &mut Robj, path: &Path) -> Result<()> { } fn add_model_source_attr(model_robj: &mut Robj, path: &Path) -> Result<()> { - let source_path = get_model_source_path(path)?; + let source_path = to_config_relative(path)?; model_robj .set_attrib("model_source", source_path.into_robj()) .map_to_extendr_err("Failed to set model source attribute")?; @@ -101,7 +101,7 @@ pub fn robj_to_model(model: &Robj) -> Result { /// } #[extendr] pub fn read_model(path: &str) -> Result { - let mod_path = resolve_input_model_path(&path)?; + let mod_path = validate_model_path(&path)?; let content = fs::read_to_string(&mod_path).map_to_extendr_err("")?; let mut model = Model::parse(&content).map_to_extendr_err("Failed to read model file")?; @@ -143,7 +143,7 @@ pub fn check_dataset(model: Robj) -> Result { .ok_or_extendr_err("Model object is missing model_source attribute")? .as_str() .ok_or_extendr_err("model_source attribute must be a character")?; - let model_path = resolve_model_source_path(source)?; + let model_path = from_config_relative(source)?; let model_dir = model_path .parent() .ok_or_extendr_err("Could not determine model directory")?; diff --git a/src/rust/nonmem/src/model/run_status.rs b/src/rust/nonmem/src/model/run_status.rs index 0800482..3774fb1 100644 --- a/src/rust/nonmem/src/model/run_status.rs +++ b/src/rust/nonmem/src/model/run_status.rs @@ -6,7 +6,7 @@ use extendr_api::prelude::*; use hyperion_core::{OptionExt, extendr_err}; -use crate::utils::{find_output_file, path_from_robj, resolve_model_source_path}; +use crate::utils::{find_output_file, from_config_relative, path_from_robj}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RunStatus { @@ -68,7 +68,7 @@ pub fn get_run_status(input: Robj) -> Result { let source_str = source .as_str() .ok_or_extendr_err("model_source attribute must be a string")?; - resolve_model_source_path(source_str)? + from_config_relative(source_str)? } else { path_from_robj(&input)? }; diff --git a/src/rust/nonmem/src/model/summary.rs b/src/rust/nonmem/src/model/summary.rs index 763483a..c5cb339 100644 --- a/src/rust/nonmem/src/model/summary.rs +++ b/src/rust/nonmem/src/model/summary.rs @@ -15,8 +15,8 @@ use nonmem::output_files::{ use crate::{ output_files::{OMEGA, ParameterRowBuilder, SIGMA, THETA, build_parameters_df}, utils::{ - find_output_file, get_comment_type, path_from_robj, resolve_input_model_path, - resolve_model_input_path_from_robj, + find_output_file, get_comment_type, path_from_robj, validate_model_path, + validated_model_from_robj, }, }; use hyperion_core::{OptionExt, ResultExt, extendr_err}; @@ -285,14 +285,14 @@ fn parse_summary_directory(input: Robj) -> Result { return Ok(path.to_path_buf()); } if path.exists() { - let model_path = resolve_input_model_path(path)?; + let model_path = validate_model_path(path)?; return run_dir_from_model_path(&model_path); } return Err(extendr_err!("Path does not exist: {}", path.display())); } if input.inherits("hyperion_nonmem_model") { - let model_path = resolve_model_input_path_from_robj(&input)?; + let model_path = validated_model_from_robj(&input)?; return run_dir_from_model_path(&model_path); } diff --git a/src/rust/nonmem/src/utils.rs b/src/rust/nonmem/src/utils.rs index 610b4d8..531f125 100644 --- a/src/rust/nonmem/src/utils.rs +++ b/src/rust/nonmem/src/utils.rs @@ -98,8 +98,9 @@ pub fn find_output_file(input_path: impl AsRef, extension: &str) -> Result } } -/// Resolve a model input path (.mod or .ctl), with a fallback for output-model paths. -pub fn resolve_input_model_path(input_path: impl AsRef) -> Result { +/// Validate and resolve a model input path (.mod or .ctl). +/// Returns error if path is not a .mod/.ctl file or doesn't exist. +pub fn validate_model_path(input_path: impl AsRef) -> Result { let path = input_path.as_ref(); if path.is_dir() { @@ -147,8 +148,9 @@ pub fn resolve_input_model_path(input_path: impl AsRef) -> Result Err(extendr_err!("File not found: {}", path.display())) } -/// Builds a model source string relative to the pharos config directory when available. -pub fn get_model_source_path(path: impl AsRef) -> Result { +/// Convert an absolute path to be relative to the pharos config directory. +/// Returns the original path if no config directory is found. +pub fn to_config_relative(path: impl AsRef) -> Result { let path = path.as_ref(); let config_dir = find_config_dir().map_to_extendr_err("Failed to find config dir")?; @@ -160,8 +162,9 @@ pub fn get_model_source_path(path: impl AsRef) -> Result { Ok(path.to_string_lossy().to_string()) } -/// Resolve a model source string into an absolute or config-relative path. -pub fn resolve_model_source_path(source: impl AsRef) -> Result { +/// Convert a config-relative path to an absolute path. +/// If the path is already absolute, returns it unchanged. +pub fn from_config_relative(source: impl AsRef) -> Result { let source_path = source.as_ref(); if source_path.is_absolute() { return Ok(source_path.to_path_buf()); @@ -174,26 +177,37 @@ pub fn resolve_model_source_path(source: impl AsRef) -> Result { Ok(source_path.to_path_buf()) } -/// Resolve a model object's model_source attribute to an input model path. -pub fn resolve_model_input_path_from_robj(model: &Robj) -> Result { +/// Extract model_source attribute from Robj and resolve to absolute path. +/// Validates that the path is a .mod/.ctl file. +fn model_path_from_robj(model: &Robj) -> Result { let source = model .get_attrib("model_source") .ok_or_extendr_err("Model object is missing model_source attribute")?; let source_str = source .as_str() .ok_or_extendr_err("model_source attribute must be a string")?; - let source_path = resolve_model_source_path(source_str)?; - resolve_input_model_path(source_path) + let source_path = from_config_relative(source_str)?; + validate_model_path(source_path) } -/// Resolve input to a PathBuf for use with find_output_file. -/// -/// Accepts either a path string or hyperion_nonmem_model object. -/// Does not validate the path - suitable for functions that accept -/// directories, output files, or model files and use find_output_file. +/// Extract model_source attribute from Robj and resolve to absolute path. +/// Does NOT validate file extension - use for paths that may be .lst, directories, etc. +pub fn model_source_from_robj(model: &Robj) -> Result { + let source = model + .get_attrib("model_source") + .ok_or_extendr_err("Model object is missing model_source attribute")?; + let source_str = source + .as_str() + .ok_or_extendr_err("model_source attribute must be a string")?; + from_config_relative(source_str) +} + +/// Extract a path from an Robj (either a string path or hyperion_nonmem_model object). +/// Does NOT validate the path - suitable for functions that accept +/// directories, output files, or model files. pub fn path_from_robj(input: &Robj) -> Result { if input.inherits("hyperion_nonmem_model") { - return resolve_model_input_path_from_robj(input); + return model_source_from_robj(input); } if let Some(s) = input.as_str() { @@ -205,11 +219,19 @@ pub fn path_from_robj(input: &Robj) -> Result { )) } -/// Resolve input that can be either a path string or hyperion_nonmem_model object. -/// Validates that the path is a .mod or .ctl input model file. -pub fn resolve_model_or_path(input: Robj) -> Result { - let path = path_from_robj(&input)?; - resolve_input_model_path(&path) +/// Extract a path from an Robj and validate it's a .mod/.ctl model file. +pub fn validated_model_from_robj(input: &Robj) -> Result { + if input.inherits("hyperion_nonmem_model") { + return model_path_from_robj(input); + } + + if let Some(s) = input.as_str() { + return validate_model_path(s); + } + + Err(extendr_err!( + "Input must be a path or a hyperion_nonmem_model object" + )) } fn make_relative_path(base: &Path, target: &Path) -> PathBuf { @@ -366,31 +388,46 @@ pub fn get_comment_type_wrap() -> Result { Ok(robj) } +/// Validate and resolve a model path (.mod or .ctl). +/// /// @keywords internal /// @noRd -#[extendr(r_name = "resolve_input_model_path")] -pub fn resolve_input_model_path_wrap(path: &str) -> Result { - let path = resolve_input_model_path(path)?; +#[extendr(r_name = "validate_model_path")] +pub fn validate_model_path_wrap(path: &str) -> Result { + let path = validate_model_path(path)?; Ok(path.to_string_lossy().into_robj()) } -/// Resolve a model_source string into an absolute or config-relative path. +/// Convert a config-relative path to absolute. /// /// @keywords internal /// @noRd -#[extendr(r_name = "resolve_model_source_path")] -pub fn resolve_model_source_path_wrap(path: &str) -> Result { - let path = resolve_model_source_path(path)?; +#[extendr(r_name = "from_config_relative")] +pub fn from_config_relative_wrap(path: &str) -> Result { + let path = from_config_relative(path)?; Ok(path.to_string_lossy().into_robj()) } +/// Convert an absolute path to be relative to the pharos config directory. +/// +/// @param path Absolute path to make relative. +/// @return Path relative to pharos.toml directory, or original path if not under config dir. +/// @keywords internal +/// @noRd +#[extendr(r_name = "to_config_relative")] +pub fn to_config_relative_wrap(path: &str) -> Result { + let rel_path = to_config_relative(path)?; + Ok(rel_path.into_robj()) +} + extendr_module! { mod utils; fn get_pharos_config; fn get_comment_type_wrap; - fn resolve_input_model_path_wrap; - fn resolve_model_source_path_wrap; + fn validate_model_path_wrap; + fn from_config_relative_wrap; + fn to_config_relative_wrap; } #[cfg(test)] @@ -504,47 +541,47 @@ mod tests { } #[test] - fn test_resolve_input_model_path_ok() { + fn test_validate_model_path_ok() { let temp_dir = TempDir::new().unwrap(); let mod_file = temp_dir.path().join("run001.mod"); fs::write(&mod_file, "test content").unwrap(); - let result = resolve_input_model_path(&mod_file).unwrap(); + let result = validate_model_path(&mod_file).unwrap(); assert_eq!(result, mod_file); } #[test] - fn test_resolve_input_model_path_rejects_output_model() { + fn test_validate_model_path_rejects_output_model() { let temp_dir = TempDir::new().unwrap(); let run_dir = temp_dir.path().join("run001"); fs::create_dir(&run_dir).unwrap(); let output_mod = run_dir.join("run001.mod"); fs::write(&output_mod, "test content").unwrap(); - let err = resolve_input_model_path(&output_mod).unwrap_err(); + let err = validate_model_path(&output_mod).unwrap_err(); let message = format!("{err}"); assert!(message.contains("Expected input model file")); assert!(message.contains("Try:")); } #[test] - fn test_resolve_input_model_path_rejects_wrong_extension() { + fn test_validate_model_path_rejects_wrong_extension() { let temp_dir = TempDir::new().unwrap(); let txt_file = temp_dir.path().join("run001.txt"); fs::write(&txt_file, "test content").unwrap(); - let err = resolve_input_model_path(&txt_file).unwrap_err(); + let err = validate_model_path(&txt_file).unwrap_err(); let message = format!("{err}"); assert!(message.contains("Expected .mod or .ctl")); } #[test] - fn test_resolve_model_source_path_absolute() { + fn test_from_config_relative_absolute() { let temp_dir = TempDir::new().unwrap(); let mod_file = temp_dir.path().join("run001.mod"); fs::write(&mod_file, "test content").unwrap(); - let result = resolve_model_source_path(mod_file.to_string_lossy().as_ref()).unwrap(); + let result = from_config_relative(mod_file.to_string_lossy().as_ref()).unwrap(); assert_eq!(result, mod_file); } } diff --git a/src/rust/scheduler/src/lib.rs b/src/rust/scheduler/src/lib.rs index 6887604..cd890b6 100644 --- a/src/rust/scheduler/src/lib.rs +++ b/src/rust/scheduler/src/lib.rs @@ -12,7 +12,7 @@ use scheduler::{ }; use hyperion_core::{ResultExt, extendr_err}; -use hyperion_nonmem::utils::{load_nonmem_config, resolve_model_input_path_from_robj}; +use hyperion_nonmem::utils::{load_nonmem_config, validated_model_from_robj}; /// Helper function to process Robj model input and expand patterns /// @@ -30,7 +30,7 @@ fn process_model_robj(model: Robj) -> Result> { // Handle hyperion_nonmem_model object if model.inherits("hyperion_nonmem_model") { - let path = resolve_model_input_path_from_robj(&model)?; + let path = validated_model_from_robj(&model)?; return Ok(vec![path]); } @@ -47,7 +47,7 @@ fn process_model_robj(model: Robj) -> Result> { // Handle R lists (can contain strings or model objects) list.values().try_fold(Vec::new(), |mut acc, item| { if item.inherits("hyperion_nonmem_model") { - let path = resolve_model_input_path_from_robj(&item)?; + let path = validated_model_from_robj(&item)?; acc.push(path); Ok(acc) } else if let Some(pattern) = item.as_str() { From ce4057e07c3d061add89fc34e1a5ff31047a50f3 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 29 Jan 2026 21:05:12 -0500 Subject: [PATCH 4/4] updated NEWS --- NEWS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/NEWS.md b/NEWS.md index 08e732f..bcc21db 100644 --- a/NEWS.md +++ b/NEWS.md @@ -45,11 +45,22 @@ - Expand NONMEM example data bundled with the package. - Refresh documentation, man pages, and vignettes to cover new APIs and examples. +### Model Utilities + +- New model accessor functions: + - `get_model_name()` - Get the model name (filename without extension) + - `get_model_dir()` - Get the model directory path (relative to pharos.toml) + - `get_data_path()` - Get the dataset path from the model +- `check_dataset()` now automatically derives the model directory from the + `model_source` attribute, removing the need for the `model_dir` argument. + ## Breaking Changes - `get_model_summary()` is deprecated in favor of `summary(mod)`. - Parameter data frame column renamed: `value` is now `estimate`. - Test data relocated from `vignettes/test_data/` to `inst/extdata/`. +- `check_dataset()` no longer accepts a `model_dir` argument; the directory is + now derived automatically from the model's `model_source` attribute. ## Dependencies