diff --git a/NAMESPACE b/NAMESPACE
index 1ee0aa2..5a5282f 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)
@@ -33,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)
@@ -40,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/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
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
new file mode 100644
index 0000000..fc45fe9
--- /dev/null
+++ b/R/dataset.R
@@ -0,0 +1,41 @@
+#' 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 <- to_config_relative(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 <- to_config_relative(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..e2751a9 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)
#'
@@ -421,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 6ff888b..bcdbe0b 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,76 @@ 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",
+ "get_model_name",
+ "get_model_dir",
+ "get_data_path",
] },
- { 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/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/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/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 45bba3e..1b47746 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;
@@ -12,9 +12,10 @@ 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,
+ find_output_file, from_config_relative, get_comment_type, to_config_relative,
+ validate_model_path,
};
-use hyperion_core::ResultExt;
+use hyperion_core::{OptionExt, ResultExt};
pub mod check;
pub mod copy;
@@ -54,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")?;
@@ -100,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")?;
@@ -126,24 +127,34 @@ 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 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 = from_config_relative(source)?;
+ 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/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 33967f5..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,9 +162,10 @@ 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: &str) -> Result {
- let source_path = Path::new(source);
+/// 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: &str) -> 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() {
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()
```