diff --git a/.Rbuildignore b/.Rbuildignore index 83c09f98..5cf569b2 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -24,3 +24,4 @@ ^doc$ ^Meta$ ^_starlightr\.toml$ +^\.positai$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index d5d16aa6..ca81c09b 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -39,11 +39,25 @@ jobs: run: | git config --global url."https://${{ secrets.PRIVATE_REPO_ACCESS_TOKEN }}@github.com/".insteadOf "https://github.com/" - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.config.rust-version }} targets: ${{ matrix.config.rust-target }} + - name: Ensure rustup cargo bin is first on PATH + if: runner.os != 'Windows' + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Verify Rust toolchain + if: runner.os != 'Windows' + run: | + which rustup + which rustc + which cargo + rustup show + rustc --version + cargo --version + # Add rust-cache for faster builds - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -95,7 +109,7 @@ jobs: echo "All system dependencies are present" fi - name: rv-sync - run: rv sync + run: rv sync -vvv if: runner.os != 'Windows' - name: rv-sync (Windows) diff --git a/.github/workflows/pharos-dependency-check.yaml b/.github/workflows/pharos-dependency-check.yaml index d6176233..e4095c24 100644 --- a/.github/workflows/pharos-dependency-check.yaml +++ b/.github/workflows/pharos-dependency-check.yaml @@ -15,14 +15,29 @@ jobs: - name: Check pharos dependency status run: | - # Get pharos branch from Cargo.toml + # Determine whether Cargo.toml pins pharos via branch or tag. + # Branch pin: compare lock commit to that branch's HEAD. + # Tag pin: compare lock commit to main's HEAD (so we still get + # notified when pharos advances past a release pin). BRANCH=$(grep -A 2 'git = "https://github.com/a2-ai/pharos"' src/rust/Cargo.toml | grep 'branch =' | sed 's/.*branch = "\([^"]*\)".*/\1/' | head -1) + TAG=$(grep -A 2 'git = "https://github.com/a2-ai/pharos"' src/rust/Cargo.toml | grep 'tag =' | sed 's/.*tag = "\([^"]*\)".*/\1/' | head -1) - # Get current and latest commits CURRENT=$(grep -A 1 'source = "git+https://github.com/a2-ai/pharos' src/rust/Cargo.lock | grep -o '#[a-f0-9]\{40\}' | head -1 | cut -c2-) - LATEST=$(git ls-remote https://github.com/a2-ai/pharos refs/heads/$BRANCH | cut -f1) - echo "Branch: $BRANCH" + if [ -n "$BRANCH" ]; then + TARGET_REF="refs/heads/$BRANCH" + TARGET_LABEL="branch $BRANCH" + elif [ -n "$TAG" ]; then + TARGET_REF="refs/heads/main" + TARGET_LABEL="main (pinned to tag $TAG)" + else + echo "::error::Could not determine pharos pin style (branch or tag) in Cargo.toml" + exit 1 + fi + + LATEST=$(git ls-remote https://github.com/a2-ai/pharos $TARGET_REF | cut -f1) + + echo "Pin: $TARGET_LABEL" echo "Current: $CURRENT" echo "Latest: $LATEST" diff --git a/.gitignore b/.gitignore index 654856b2..b6d42449 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ lefthook.yaml ./pharos.toml /doc/ /Meta/ +.positai diff --git a/DESCRIPTION b/DESCRIPTION index de61840f..bc57b02a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: hyperion Title: Pharmaceutical Model Development and Workflow Tools -Version: 0.4.2 +Version: 0.5.0 Authors@R: c( person("Matt", "Smith", , "matthews@a2-ai.com", role = c("aut", "cre")), person("Vincent", "Prouillet", ,"vincent@a2-ai.com", role = c("aut")), @@ -17,8 +17,7 @@ Description: Hyperion is an R interface to the pharos CLI for License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 -Config/rextendr/version: 0.4.2 +Config/rextendr/version: 0.5.0 Config/build/never-clean: true Config/build/extra-sources: src/rust/Cargo.lock SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz @@ -33,7 +32,6 @@ Suggests: withr VignetteBuilder: knitr Imports: - fs, cli, knitr, lifecycle, @@ -42,3 +40,5 @@ Imports: tomledit Config/testthat/edition: 3 URL: https://a2-ai.github.io/hyperion-docs, https://github.com/a2-ai/hyperion +Config/roxygen2/version: 7.3.3 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index cb6c4438..c8257788 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,17 +1,17 @@ # Generated by roxygen2: do not edit by hand -S3method(base::`$`, hyperion_nonmem_model) -S3method(base::`[[`, hyperion_nonmem_model) -S3method(base::names, hyperion_nonmem_model) -S3method(base::print, hyperion_model_metadata) -S3method(base::print, hyperion_nonmem_dataset) -S3method(base::print, hyperion_nonmem_model) -S3method(base::print, hyperion_nonmem_summary) -S3method(base::print, hyperion_nonmem_summary_not_run) -S3method(base::print, hyperion_nonmem_summary_running) -S3method(base::print, hyperion_nonmem_tree) -S3method(base::print, parameter_audit) -S3method(base::summary, hyperion_nonmem_model) +S3method(base::`$`,hyperion_nonmem_model) +S3method(base::`[[`,hyperion_nonmem_model) +S3method(base::names,hyperion_nonmem_model) +S3method(base::print,hyperion_model_metadata) +S3method(base::print,hyperion_nonmem_dataset) +S3method(base::print,hyperion_nonmem_model) +S3method(base::print,hyperion_nonmem_summary) +S3method(base::print,hyperion_nonmem_summary_not_run) +S3method(base::print,hyperion_nonmem_summary_running) +S3method(base::print,hyperion_nonmem_tree) +S3method(base::print,parameter_audit) +S3method(base::summary,hyperion_nonmem_model) S3method(knitr::knit_print,hyperion_model_metadata) S3method(knitr::knit_print,hyperion_nonmem_dataset) S3method(knitr::knit_print,hyperion_nonmem_model) @@ -20,7 +20,7 @@ S3method(knitr::knit_print,hyperion_nonmem_summary_not_run) S3method(knitr::knit_print,hyperion_nonmem_summary_running) S3method(knitr::knit_print,hyperion_nonmem_tree) S3method(knitr::knit_print,parameter_audit) -S3method(utils::str, hyperion_nonmem_model) +S3method(utils::str,hyperion_nonmem_model) export(ModelComments) export(OmegaComment) export(SigmaComment) @@ -32,13 +32,13 @@ export(are_models_in_lineage) export(audit_parameter_info) export(check_dataset) export(check_model) +export(clear_metadata_file) export(compute_ci) export(compute_cv) export(compute_rse) export(copy_model) export(format_hyperion_decimal_string) export(format_hyperion_sigfig_string) -export(format_omega_display_name) export(from_config_relative) export(get_comment) export(get_comment_type) @@ -74,6 +74,7 @@ export(submit_model_to_slurm) export(transform_value) export(update_metadata_file) export(update_param_info) +export(use_comments) export(use_type1_comments) if (getRversion() < "4.3.0") importFrom("S7", "@") importFrom(knitr,knit_print) diff --git a/NEWS.md b/NEWS.md index a87171c7..57b4059d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,66 @@ +# hyperion 0.5.0 + +## Breaking changes + +- `get_model_lineage()` signature changed. Old: `get_model_lineage(model_dir)` + (required a directory or model object). New: `get_model_lineage(model = NULL, + from = NULL, to = NULL)`. + - No-arg call returns the whole project lineage tree, rooted at the directory + containing `pharos.toml`. + - `model` returns ancestors and descendants of one model. + - `from`/`to` filter the tree downstream / upstream / to the slice between + two models. + - The `model_dir` argument no longer exists. +- `copy_model()` `description` is now required (was `description = NULL`). +- R-side raw comment parsing is removed. All comment parsing is owned by + pharos. The "raw" mode documentation, parsing pipeline, and transform + keyword mapping are gone from `get_model_parameter_info()`. `NA` names are + acceptable when pharos cannot parse a comment; `summary()` falls back to + NONMEM names in that case. Raw parsing mode is most similar to `type2` + comments in pharos. +- `use_type1_comments()` is soft-deprecated in favor of `use_comments()`. + It still works but emits a `.Deprecated()` warning and delegates. + +## New features + +- `clear_metadata_file()` — new exported function. Selectively clears + `based_on`, `copied_from`, and/or `tags` in a model's metadata file; + unspecified fields are preserved. +- `copy_model()` gains `based_on` and `tags` arguments so metadata can be + populated at copy time rather than via a follow-up `set_metadata_file()`. +- `set_metadata_file()` gains `copied_from` to record mechanical-copy + provenance separately from `based_on`. +- `hyperion.config_dir` option — explicit override for where `pharos.toml` is + resolved from. Status is surfaced in the package-load options message. +- `use_comments(type = c("type1", "type2"))` — single entry point for setting + comment parsing type in `pharos.toml`. The new `"type2"` mode is a flexible + structured grammar; `"type1"` remains strict structured. Replaces + `use_type1_comments()`. +- `summary()` includes `model_file` in its output. +- `read_ext_file()` accepts a `hyperion_nonmem_model` object in addition to + the previously-supported path forms. +- `get_model_lineage(verbose = TRUE)` renders the lineage as a flat table + with Model, Parent, Description, Tags, Model Hash, and Dataset Hash columns + instead of the tree view. + +## Bug fixes + +- `.ext` parameter columns are sanitized to syntactic R names (e.g. + `IIV (CL)` → `IIV_CL`), so returned data frames no longer require backtick + quoting. +- `.grd` gradient column names are similarly sanitized; the surrounding + `GRD(...)` wrapper is stripped before sanitization. +- Fixed `clear_metadata_file()` so previously-set values are actually cleared + (prior behavior left stale fields in place). +- `read_model()` reports parsing diagnostics for malformed NONMEM control + streams, backed by a new pharos parser aimed at better control-stream + coverage. +- `$EST FILE=` overrides are now honored when locating `.ext` files in + `get_parameters()`, `read_ext_file()`, `get_run_status()`, and + `copy_model()` parameter updates. Previously these always used + `{model}.ext` regardless of where NONMEM was actually writing estimates. + + # hyperion 0.4.2 ## Bug fixes diff --git a/R/comments-audit.R b/R/comments-audit.R index 102f259a..24eee3b2 100644 --- a/R/comments-audit.R +++ b/R/comments-audit.R @@ -44,7 +44,7 @@ audit_parameter_info <- function(info) { #' @param x A parameter_audit object #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, parameter_audit) +#' @exportS3Method base::print parameter_audit print.parameter_audit <- function(x, ...) { cli::cli_text("") cli::cli_h1("Parameter Info Audit") diff --git a/R/comments-classes.R b/R/comments-classes.R index 12e728b2..edc51410 100644 --- a/R/comments-classes.R +++ b/R/comments-classes.R @@ -1,39 +1,3 @@ -#' Map raw parameterization string to Transform name -#' -#' @param raw_param Raw parameterization string from comment (e.g., "EXP", ":EXP") -#' @param kind Parameter kind: "THETA", "OMEGA", or "SIGMA" -#' @return Mapped parameterization name or NULL if not recognized -#' @noRd -map_parameterization <- function(raw_param, kind) { - if (is.null(raw_param) || !nzchar(raw_param)) { - return(NULL) - } - - # Remove leading colon if present and convert to uppercase - cleaned <- toupper(gsub("^:", "", trimws(raw_param))) - - # Common mappings for all parameter types - switch( - EXPR = cleaned, - "EXP" = "LogNormal", - "LOG" = "LogNormal", - "LOGNORMAL" = "LogNormal", - "LOGIT" = "Logit", - "ADD" = "AddErr", - "ADDERR" = "AddErr", - "ADDITIVE" = "AddErr", - "LOGADD" = "LogAddErr", - "LOGADDERR" = "LogAddErr", - "LOGERR" = "LogAddErr", - "PROP" = "Proportional", - "PROPORTIONAL" = "Proportional", - "IDENTITY" = "Identity", - "NORMAL" = "Identity", - "NONE" = "Identity", - NULL - ) -} - #' @noRd ParameterComment <- S7::new_class( "ParameterComment", @@ -92,96 +56,6 @@ make_tracked_property <- function(field_name, valid_values = NULL) { ) } -#' Normalize omega associated_theta values against theta names -#' -#' If no exact match is found, tries matching by stripping trailing "/...". -#' Only applies when the base name maps unambiguously to a single theta name. -#' -#' @param assoc Character vector of associated theta names -#' @param theta_names Character vector of theta names -#' @return Character vector of normalized associated theta names -#' @noRd -normalize_associated_theta <- function(assoc, theta_names) { - if (length(theta_names) == 0 || length(assoc) == 0) { - return(assoc) - } - - exact_lookup <- stats::setNames(theta_names, tolower(theta_names)) - - base_names <- sub("/.*$", "", theta_names) - base_lc <- tolower(base_names) - base_map <- list() - for (i in seq_along(theta_names)) { - base_map[[base_lc[i]]] <- c(base_map[[base_lc[i]]], theta_names[i]) - } - base_lookup <- vapply( - base_map, - function(vals) { - if (length(unique(vals)) == 1) unique(vals) else NA_character_ - }, - character(1) - ) - base_lookup <- base_lookup[!is.na(base_lookup)] - - vapply( - assoc, - function(theta) { - key <- tolower(theta) - if (key %in% names(exact_lookup)) { - exact_lookup[[key]] - } else if (key %in% names(base_lookup)) { - base_lookup[[key]] - } else { - theta - } - }, - character(1) - ) -} - -#' Rename duplicate omega names by appending associated_theta -#' -#' When multiple omega comments share the same name, renames ALL of them to -#' `{name}-{associated_theta}` to ensure uniqueness. -#' -#' @param omega List of OmegaComment objects -#' @return Modified list with duplicate names renamed -#' @noRd -rename_duplicate_omega_names <- function(omega) { - if (length(omega) == 0) { - return(omega) - } - - omega_names <- vapply( - omega, - function(c) if (is.null(c@name)) NA_character_ else c@name, - character(1) - ) - - # Find names that appear more than once (excluding NA) - name_counts <- table(omega_names[!is.na(omega_names)]) - dup_names <- names(name_counts[name_counts > 1]) - - if (length(dup_names) == 0) { - return(omega) - } - - lapply(omega, function(comment) { - if (!is.null(comment@name) && comment@name %in% dup_names) { - assoc <- comment@associated_theta - if (!is.null(assoc) && length(assoc) == 1 && nzchar(assoc)) { - new_name <- paste0(comment@name, "-", assoc) - comment@name <- new_name - sources <- attr(comment, "sources") %||% list() - name_source <- sources[["name"]] %||% "default" - sources[["name"]] <- paste0("renamed from ", name_source) - attr(comment, "sources") <- sources - } - } - comment - }) -} - #' Theta parameter comment class #' #' Represents parsed comments for THETA parameters. @@ -226,6 +100,8 @@ ThetaComment <- S7::new_class( #' #' @param nonmem_name Character. The NONMEM parameter name (e.g., "OMEGA(1,1)"). #' @param name Character or NULL. User-defined parameter name (e.g., "IIV-CL"). +#' @param raw_name Character or NULL. Raw user label before pharos composition; +#' e.g. `"IIV"` when `name` is `"IIV (CL)"`. #' @param display Character or NULL. Display name for tables/output. #' @param description Character or NULL. Description of the parameter. #' @param parameterization Character or NULL. Transformation type. @@ -235,6 +111,8 @@ ThetaComment <- S7::new_class( #' \describe{ #' \item{nonmem_name}{The NONMEM parameter identifier.} #' \item{name}{User-friendly name parsed from comments.} +#' \item{raw_name}{Raw user label before pharos composition; e.g. `"IIV"` +#' when `name` is `"IIV (CL)"`.} #' \item{display}{Display name for tables. Falls back to `name` if NULL.} #' \item{description}{Longer description of what the parameter represents.} #' \item{parameterization}{Transformation type. Valid values: @@ -250,6 +128,7 @@ OmegaComment <- S7::new_class( parent = ParameterComment, properties = list( name = make_tracked_property("name"), + raw_name = make_tracked_property("raw_name"), display = make_tracked_property("display"), description = make_tracked_property("description"), parameterization = make_tracked_property( @@ -385,11 +264,7 @@ ModelComments <- S7::new_class( comment <- omega_comments[[omega_name]] if (!is.null(comment@associated_theta)) { assoc <- comment@associated_theta - sources <- attr(comment, "sources") %||% list() - assoc_source <- sources[["associated_theta"]] %||% "default" - assoc_norm <- normalize_associated_theta(assoc, theta_names) - # Validate against normalized associated_theta without mutating state. - missing <- setdiff(assoc_norm, theta_names) + missing <- setdiff(assoc, theta_names) if (length(missing) > 0) { errors <- c( errors, @@ -458,37 +333,6 @@ ModelComments <- S7::new_class( NULL }, constructor = function(theta = list(), omega = list(), sigma = list()) { - if (length(theta) > 0 && length(omega) > 0) { - theta_names <- vapply( - theta, - function(c) if (is.null(c@name)) NA_character_ else c@name, - character(1) - ) - theta_names <- theta_names[!is.na(theta_names)] - if (length(theta_names) > 0) { - omega <- lapply(omega, function(comment) { - if (!is.null(comment@associated_theta)) { - assoc <- comment@associated_theta - sources <- attr(comment, "sources") %||% list() - assoc_source <- sources[["associated_theta"]] %||% "default" - assoc_norm <- normalize_associated_theta(assoc, theta_names) - assoc_norm <- unname(assoc_norm) - if (!identical(unname(assoc), assoc_norm)) { - comment@associated_theta <- assoc_norm - sources[["associated_theta"]] <- paste0( - "normalized from ", - assoc_source - ) - attr(comment, "sources") <- sources - } - } - comment - }) - } - } - - omega <- rename_duplicate_omega_names(omega) - S7::new_object( S7::S7_object(), theta = theta, @@ -527,20 +371,36 @@ find_parameter <- function(info, parameter, kind = NULL) { # 2. Match by @name property for (key in names(comments)) { if (identical(comments[[key]]@name, parameter)) { - matches <- c( - matches, - list(list(slot = slot, key = key, obj = comments[[key]])) - ) + if ( + !any(vapply( + matches, + function(m) m$slot == slot && m$key == key, + logical(1) + )) + ) { + matches <- c( + matches, + list(list(slot = slot, key = key, obj = comments[[key]])) + ) + } } } # 3. Match by @display property for (key in names(comments)) { if (identical(comments[[key]]@display, parameter)) { - matches <- c( - matches, - list(list(slot = slot, key = key, obj = comments[[key]])) - ) + if ( + !any(vapply( + matches, + function(m) m$slot == slot && m$key == key, + logical(1) + )) + ) { + matches <- c( + matches, + list(list(slot = slot, key = key, obj = comments[[key]])) + ) + } } } } @@ -610,7 +470,16 @@ update_param_info <- function( param_obj@description <- description } if (!is.null(parameterization)) { - param_obj@parameterization <- parameterization + mapped <- map_parameterization(parameterization) + if (is.na(mapped)) { + rlang::abort(paste0( + "Invalid parameterization: ", + parameterization, + ". Valid values: ", + paste(valid_parameterizations(), collapse = ", ") + )) + } + param_obj@parameterization <- mapped } # THETA/SIGMA: unit diff --git a/R/comments-fields.R b/R/comments-fields.R index ed374799..3d5d90e1 100644 --- a/R/comments-fields.R +++ b/R/comments-fields.R @@ -7,6 +7,7 @@ theta_fields <- function() { omega_fields <- function() { c( "name", + "raw_name", "display", "description", "parameterization", diff --git a/R/comments-lookup.R b/R/comments-lookup.R index 596b696e..f196588a 100644 --- a/R/comments-lookup.R +++ b/R/comments-lookup.R @@ -95,17 +95,14 @@ apply_lookup_defaults <- function(comment, lookup_path) { } } - if (is.null(comment@parameterization) && !is.null(entry$parameterization)) { + if ( + is.null(comment@parameterization) && + !is.null(entry$parameterization) && + !is.na(entry$parameterization) + ) { if (entry$parameterization != "none") { - kind <- if (is_theta) { - "THETA" - } else if (is_omega) { - "OMEGA" - } else { - "SIGMA" - } - mapped <- map_parameterization(entry$parameterization, kind) - if (!is.null(mapped)) { + mapped <- map_parameterization(entry$parameterization) + if (!is.na(mapped)) { comment@parameterization <- mapped attr(comment, "sources")$parameterization <- lookup_path } @@ -190,10 +187,20 @@ add_parameter_to_lookup <- function( rlang::abort("name is required") } + # Treat NA the same as NULL (no value supplied) + if (!is.null(parameterization) && is.na(parameterization)) { + parameterization <- NULL + } + + # Treat "none" as "skip this field", symmetric with unit = "none" handling + if (!is.null(parameterization) && identical(parameterization, "none")) { + parameterization <- NULL + } + # Validate parameterization if provided if (!is.null(parameterization)) { - mapped <- map_parameterization(parameterization, "THETA") - if (is.null(mapped)) { + mapped <- map_parameterization(parameterization) + if (is.na(mapped)) { rlang::abort(paste0( "Invalid parameterization: ", parameterization, diff --git a/R/comments-parsing.R b/R/comments-parsing.R deleted file mode 100644 index 7a3d088d..00000000 --- a/R/comments-parsing.R +++ /dev/null @@ -1,1227 +0,0 @@ -#' Make a path relative to project root (pharos.toml directory) -#' @noRd -relative_path <- function(path) { - if (is.null(path) || path == "default" || path == "user supplied") { - return(path) - } - tryCatch( - { - config_path <- find_pharos_config_file() - if (grepl("No pharos.toml", config_path)) { - return(path) - } - root <- fs::path_dir(config_path) - as.character(fs::path_rel(path, start = root)) - }, - error = function(e) path - ) -} - -#' Set source paths for comment fields -#' -#' Always initializes the sources attribute to mark object as "initialized". -#' Fields with non-NULL values get source_path; NULL fields get "default". -#' @noRd -set_sources <- function(comment, fields, source_path) { - source_path <- relative_path(source_path) - sources <- list() - for (f in fields) { - val <- S7::prop(comment, f) - if (!is.null(val)) { - sources[[f]] <- source_path - } else { - sources[[f]] <- "default" - } - } - attr(comment, "sources") <- sources - comment -} - -#' @noRd -normalize_comment_name <- function(name) { - if (!is.null(name) && (!nzchar(name) || is.na(name))) { - return(NULL) - } - name -} - -#' @noRd -create_comment_with_sources <- function(constructor, fields, mod_path, ...) { - comment <- constructor(...) - set_sources(comment, fields, mod_path) -} - -#' Find and read model from .lst file in a directory -#' @noRd -read_model_from_lst_dir <- function(dir_path) { - lst_candidates <- list.files( - dir_path, - pattern = "\\.lst$", - ignore.case = TRUE, - full.names = TRUE - ) - if (length(lst_candidates) == 0) { - rlang::abort(paste0("lst file not found in run directory: ", dir_path)) - } - read_model_from_lst(lst_candidates[1]) -} - -#' Derive output directory from model path and read from .lst file -#' @noRd -read_model_from_lst_path <- function(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) - output_dir <- file.path(parent_dir, base_name) - - if (!dir.exists(output_dir)) { - rlang::abort(paste0( - "Output directory not found for model: ", - mod_path, - "\nExpected: ", - output_dir - )) - } - - read_model_from_lst_dir(output_dir) -} - -#' Extract all parameter comments from a model as ModelComments object -#' -#' Parses parameter comments and returns structured metadata for theta, omega, -#' and sigma parameters. -#' -#' For model objects sourced from `.mod`/`.ctl` files: -#' - if run status is `"run"`, metadata is read from the corresponding `.lst` -#' - otherwise (`"not_run"`/`"running"`), metadata is read from the model file -#' -#' @param mod A hyperion_nonmem_model object or path to a run output directory -#' containing an .lst file. -#' @param lookup_path Optional path to a TOML lookup file. If provided, fills -#' NULL fields (display, description, unit, parameterization) from the lookup. -#' -#' @return A `ModelComments` object containing theta, omega, and sigma comments. -#' -#' @section Comment Parsing Modes: -#' The parsing behavior is controlled by the `pharos.toml` configuration file. -#' In the `[nonmem.comments]` section, set `type = "type1"` to enable structured -#' type1 comment parsing. If this setting is absent or set to any other value, -#' raw comment parsing is used (the default). -#' -#' **type1 mode**: Expects comments in a structured format with explicit field -#' delimiters. This mode provides more precise extraction but requires comments -#' to follow the type1 specification. -#' -#' **raw mode** (default): Flexibly parses parameter names, units, and -#' descriptions from free-form comment text. More forgiving but may be less -#' precise for complex comment structures. -#' -#' @section Raw Comment Formats (default): -#' Applies to text after `;` on lines within `$THETA`, `$OMEGA`, and `$SIGMA` -#' blocks. -#' -#' Parsing pipeline in raw mode: -#' extract transform -> strip prefix -> extract unit -> extract name. -#' -#' **THETA/SIGMA** -#' -#' General form: -#' `[PREFIX] NAME [(UNIT)] [TRANSFORM_SEP TRANSFORM]` -#' -#' Common accepted examples: -#' - `CL` -#' - `CL (L/HR)` -#' - `CL [L/HR]` -#' - `CL ;exp` -#' - `CL :LOG` -#' - `CL (L/HR) ;exp` -#' - `THETA1 CL (L/HR) ;exp` -#' - `1: CL (L/HR) ;exp` -#' -#' Notes: -#' - Prefixes are case-insensitive; colon after prefix is optional. -#' - Numeric prefixes like `1`, `1:`, `1-`, `1.` are accepted. -#' - Units are read from `()` or `[]`, and can appear anywhere in the string. -#' - Colon transform form requires leading whitespace (e.g., `CL :EXP`). -#' - THETA strips trailing punctuation from extracted name tokens; SIGMA -#' currently does not. -#' - SIGMA also supports unit in transform segment, e.g. -#' `Name ;Transform (unit)`. -#' -#' **OMEGA** -#' -#' General form: -#' `[PREFIX] NAME_PART [THETA_REF] [TRANSFORM_SEP TRANSFORM]` -#' -#' Common accepted examples: -#' - `IIV-CL :EXP` -#' - `IIV CL ;exp` -#' - `IIV on CL ;exp` -#' - `Corr CL-V ;normal` -#' - `11: IIV CL ;exp` -#' - `OMEGA(1,1): IIV CL ;exp` -#' -#' Notes: -#' - `THETA_REF` may split on `-`, `/`, `:`, `,` into multiple associated -#' thetas. -#' - If full `THETA_REF` matches a known theta name (case-insensitive), it is -#' kept whole (for example, `CL/F`). -#' - Linking words `on`, `for`, `of` are skipped in space-separated forms. -#' -#' **Transform keyword mapping** (case-insensitive): -#' - `exp`, `log`, `lognormal` -> `LogNormal` -#' - `logit` -> `Logit` -#' - `add`, `adderr`, `additive` -> `AddErr` -#' - `logadd`, `logadderr`, `logerr` -> `LogAddErr` -#' - `prop`, `proportional` -> `Proportional` -#' - `identity`, `normal`, `none` -> `Identity` -#' -#' @seealso [get_parameter_transform()], [get_theta_names()], [get_comment()] -#' @export -get_model_parameter_info <- function(mod, lookup_path = NULL) { - if (is.character(mod) && length(mod) == 1) { - mod_path <- normalizePath(mod, mustWork = FALSE) - if (!dir.exists(mod_path)) { - rlang::abort(paste0( - "mod must be a run output directory containing an .lst file: ", - mod_path - )) - } - mod <- read_model_from_lst_dir(mod_path) - } else if (inherits(mod, "hyperion_nonmem_model")) { - mod_path <- attr(mod, "model_source") %||% "unknown" - if (!identical(mod_path, "unknown")) { - mod_path <- from_config_relative(mod_path) - } - # If model was read from .mod/.ctl file: - # - use .lst for completed runs - # - keep model object for not_run/running - if (!grepl("\\.lst$", mod_path, ignore.case = TRUE)) { - run_status <- refresh_run_status(mod) - if (identical(run_status, "run")) { - if (identical(mod_path, "unknown")) { - rlang::abort( - "Cannot locate .lst for completed run: model_source attribute is missing." - ) - } - # Derive output directory from model path (e.g., run001.mod -> run001/) - mod <- read_model_from_lst_path(mod_path) - } else if (!run_status %in% c("not_run", "running")) { - rlang::abort(paste0( - "model run_status must be 'run', 'running', or 'not_run', got: ", - run_status - )) - } - } - } else { - rlang::abort( - "mod must be a hyperion_nonmem_model object or path to a run output directory containing an .lst file" - ) - } - - mod_path <- attr(mod, "model_source") %||% "unknown" - if (!identical(mod_path, "unknown")) { - mod_path <- from_config_relative(mod_path) - } - - param_names <- get_model_parameter_names(mod) - comments_data <- extract_comments(mod) - comments <- parse_comments( - param_names, - comments_data$parsed, - comments_data$raw, - mod_path - ) - - # Split into theta, omega, sigma - theta_comments <- comments[grepl("^THETA", names(comments))] - omega_comments <- comments[grepl("^OMEGA", names(comments))] - sigma_comments <- comments[grepl("^SIGMA", names(comments))] - - # Create ModelComments object (this does duplicate omega name renaming) - result <- ModelComments( - theta = theta_comments, - omega = omega_comments, - sigma = sigma_comments - ) - - # Apply lookup AFTER renaming so "IIV-CL/F" matches lookup keys - if (!is.null(lookup_path)) { - lookup_path <- normalizePath(lookup_path, mustWork = FALSE) - result <- apply_lookup(result, lookup_path) - } - - result -} - -#' @noRd -extract_comments <- function(mod) { - parsed <- list() - raw <- list() - - for (i in seq_along(mod$theta_parameters)) { - old_name <- paste0("THETA", i) - parsed[[old_name]] <- mod$theta_parameters[[i]]$parsed_comment - raw[[old_name]] <- mod$theta_parameters[[i]]$comment - } - - result <- extract_block_comments(parsed, raw, mod$omega_blocks, "OMEGA") - parsed <- result$parsed - raw <- result$raw - - result <- extract_block_comments(parsed, raw, mod$sigma_blocks, "SIGMA") - - list(parsed = result$parsed, raw = result$raw) -} - -#' @noRd -extract_block_comments <- function(parsed, raw, blocks, prefix) { - row <- 1 - - for (block in blocks) { - struct <- block$structure - - # Handle structure as string "Diagonal" or list with named element - is_diagonal <- identical(struct, "Diagonal") || - (is.list(struct) && "Diagonal" %in% names(struct)) - is_block <- is.list(struct) && "Block" %in% names(struct) - is_block_same <- is.list(struct) && "BlockSame" %in% names(struct) - - if (is_diagonal) { - for (param in block$parameters) { - old_name <- sprintf("%s(%d,%d)", prefix, row, row) - parsed[[old_name]] <- param$parsed_comment - raw[[old_name]] <- param$comment - row <- row + 1 - } - } else if (is_block) { - block_size <- struct$Block$size - param_idx <- 1 - start_row <- row - - for (i in seq_len(block_size)) { - # Track elements on this row - row_names <- character(i) - - for (j in seq_len(i)) { - old_name <- sprintf( - "%s(%d,%d)", - prefix, - start_row + i - 1, - start_row + j - 1 - ) - row_names[j] <- old_name - parsed[[old_name]] <- block$parameters[[param_idx]]$parsed_comment - raw[[old_name]] <- block$parameters[[param_idx]]$comment - param_idx <- param_idx + 1 - } - - # Clear duplicate comments from off-diagonals - # (when elements share a source line, they all get the same comment from parser) - if (i > 1) { - diag_name <- row_names[i] # Last element is diagonal (j == i) - diag_comment <- raw[[diag_name]] - if (!is.null(diag_comment) && nzchar(diag_comment)) { - for (k in seq_len(i - 1)) { - off_diag_name <- row_names[k] - if (identical(raw[[off_diag_name]], diag_comment)) { - raw[[off_diag_name]] <- NULL - parsed[[off_diag_name]] <- NULL - } - } - } - } - } - row <- start_row + block_size - } else if (is_block_same) { - block_size <- struct$BlockSame$size - row <- row + block_size - } - } - - list(parsed = parsed, raw = raw) -} - -#' Parse comments from model based on comment_type setting -#' @noRd -parse_comments <- function( - param_names, - parsed_comments, - raw_comments, - mod_path -) { - comment_type <- get_comment_type() - - if (identical(comment_type, "type1")) { - parse_type1_comments(param_names, parsed_comments, raw_comments, mod_path) - } else { - parse_raw_comments(param_names, raw_comments, mod_path) - } -} - -# ============================================================================== -# Raw comment parsing (no comment_type set, extract from raw text only) -# ============================================================================== - -#' @noRd -parse_raw_comments <- function(param_names, raw_comments, mod_path) { - nonmem_names <- names(param_names) - - # First pass: parse thetas to collect known theta names - theta_names <- nonmem_names[grepl("^THETA", nonmem_names)] - theta_comments <- lapply(theta_names, function(nonmem_name) { - name <- param_names[[nonmem_name]] - raw <- raw_comments[[nonmem_name]] - parse_raw_theta_comment(nonmem_name, name, raw, mod_path) - }) - names(theta_comments) <- theta_names - - # Collect known theta names for context - known_thetas <- vapply( - theta_comments, - function(c) c@name %||% "", - character(1) - ) - known_thetas <- known_thetas[nzchar(known_thetas)] - - # Second pass: parse omega/sigma with known_thetas context - other_names <- nonmem_names[!grepl("^THETA", nonmem_names)] - other_comments <- lapply(other_names, function(nonmem_name) { - name <- param_names[[nonmem_name]] - raw <- raw_comments[[nonmem_name]] - - if (grepl("^OMEGA", nonmem_name)) { - parse_raw_omega_comment(nonmem_name, name, raw, mod_path, known_thetas) - } else if (grepl("^SIGMA", nonmem_name)) { - parse_raw_sigma_comment(nonmem_name, name, raw, mod_path) - } else { - rlang::abort(paste0("Unknown parameter type: ", nonmem_name)) - } - }) - names(other_comments) <- other_names - - # Combine and preserve original order - comments <- c(theta_comments, other_comments) - comments[nonmem_names] -} - -#' @noRd -parse_raw_theta_comment <- function(nonmem_name, name, raw, mod_path = NULL) { - name <- normalize_comment_name(name) - - unit <- NULL - parameterization <- NULL - - if (!is.null(raw) && nzchar(raw)) { - parts <- extract_raw_theta_parts(raw) - if (is.null(name)) { - name <- parts$name - } - unit <- parts$unit - parameterization <- map_parameterization(parts$parameterization, "THETA") - } - - create_comment_with_sources( - ThetaComment, - theta_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - unit = unit, - parameterization = parameterization - ) -} - -#' @noRd -parse_raw_omega_comment <- function( - nonmem_name, - name, - raw, - mod_path = NULL, - known_thetas = NULL -) { - name <- normalize_comment_name(name) - - parameterization <- NULL - associated_theta <- NULL - - if (!is.null(raw) && nzchar(raw)) { - parts <- extract_raw_omega_parts(raw, known_thetas) - if (is.null(name)) { - name <- parts$name - } - parameterization <- map_parameterization(parts$parameterization, "OMEGA") - associated_theta <- parts$associated_theta - } - - create_comment_with_sources( - OmegaComment, - omega_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - parameterization = parameterization, - associated_theta = associated_theta - ) -} - -#' @noRd -parse_raw_sigma_comment <- function(nonmem_name, name, raw, mod_path = NULL) { - name <- normalize_comment_name(name) - - unit <- NULL - parameterization <- NULL - - if (!is.null(raw) && nzchar(raw)) { - parts <- extract_raw_sigma_parts(raw) - if (is.null(name)) { - name <- parts$name - } - unit <- parts$unit - parameterization <- map_parameterization(parts$parameterization, "SIGMA") - } - - create_comment_with_sources( - SigmaComment, - sigma_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - unit = unit, - parameterization = parameterization - ) -} - -# ============================================================================== -# Type1 comment parsing -# ============================================================================== - -#' @noRd -parse_type1_comments <- function( - param_names, - parsed_comments, - raw_comments, - mod_path -) { - nonmem_names <- names(param_names) - - # First pass: parse thetas to collect known theta names - theta_names <- nonmem_names[grepl("^THETA", nonmem_names)] - theta_comments <- lapply(theta_names, function(nonmem_name) { - name <- param_names[[nonmem_name]] - parsed <- parsed_comments[[nonmem_name]] - raw <- raw_comments[[nonmem_name]] - parse_type1_theta_comment(nonmem_name, name, parsed, raw, mod_path) - }) - names(theta_comments) <- theta_names - - # Collect known theta names for context - known_thetas <- vapply( - theta_comments, - function(c) c@name %||% "", - character(1) - ) - known_thetas <- known_thetas[nzchar(known_thetas)] - - # Second pass: parse omega/sigma with known_thetas context - other_names <- nonmem_names[!grepl("^THETA", nonmem_names)] - other_comments <- lapply(other_names, function(nonmem_name) { - name <- param_names[[nonmem_name]] - parsed <- parsed_comments[[nonmem_name]] - raw <- raw_comments[[nonmem_name]] - - if (grepl("^OMEGA", nonmem_name)) { - parse_type1_omega_comment( - nonmem_name, - name, - parsed, - raw, - mod_path, - known_thetas - ) - } else if (grepl("^SIGMA", nonmem_name)) { - parse_type1_sigma_comment(nonmem_name, name, parsed, raw, mod_path) - } else { - rlang::abort(paste0("Unknown parameter type: ", nonmem_name)) - } - }) - names(other_comments) <- other_names - - # Combine and preserve original order - comments <- c(theta_comments, other_comments) - comments[nonmem_names] -} - -#' @noRd -parse_type1_theta_comment <- function( - nonmem_name, - name, - parsed, - raw, - mod_path -) { - name <- normalize_comment_name(name) - - unit <- NULL - parameterization <- NULL - - # Try to extract from parsed comment - if (!is.null(parsed) && !is.null(parsed$Type1)) { - type1 <- parsed$Type1 - - if (!is.null(type1$WithUnit)) { - if (is.null(name)) { - name <- type1$WithUnit$parameter - } - if (is.null(unit)) { - unit <- type1$WithUnit$unit - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - type1$WithUnit$parametrization, - "THETA" - ) - } - } else if (!is.null(type1$Type)) { - if (is.null(name)) { - name <- type1$Type$typ - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - type1$Type$parameterization, - "THETA" - ) - } - } else if (!is.null(type1$Covariate)) { - if (is.null(name)) name <- type1$Covariate$parameter - } else if (is.character(type1)) { - if (is.null(name)) name <- extract_name_from_raw(type1) - } - } - - # Fallback: extract from raw comment - if (is.null(name) && !is.null(raw) && nzchar(raw)) { - name <- extract_name_from_raw(raw) - } - - create_comment_with_sources( - ThetaComment, - theta_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - unit = unit, - parameterization = parameterization - ) -} - -#' Check if an omega parameter is diagonal (variance) vs off-diagonal (covariance) -#' @noRd -is_diagonal_omega <- function(nonmem_name) { - # Parse OMEGA(i,j) format - match <- regmatches( - nonmem_name, - regexec("OMEGA\\((\\d+),(\\d+)\\)", nonmem_name) - )[[1]] - if (length(match) == 3) { - return(match[2] == match[3]) - } - # If we can't parse, assume diagonal - TRUE -} - -#' @noRd -parse_type1_omega_comment <- function( - nonmem_name, - name, - parsed, - raw, - mod_path, - known_thetas = NULL -) { - name <- normalize_comment_name(name) - - parameterization <- NULL - associated_theta <- NULL - - # Parse name format: "OM1 (CL)" to extract associated_theta - if (!is.null(name) && grepl("\\(.*\\)", name)) { - theta_part <- gsub(".*\\((.+)\\).*", "\\1", name) - # Use split_theta_reference for context-aware splitting - associated_theta <- split_theta_reference(theta_part, known_thetas) - name <- trimws(gsub("\\s*\\(.*\\)\\s*$", "", name)) - } - - # Try to extract from parsed comment - if (!is.null(parsed) && !is.null(parsed$Type1)) { - type1 <- parsed$Type1 - - if (is.character(type1)) { - # Type1$Unknown: raw string stored directly - if (is.null(name)) { - parsed_raw <- extract_raw_omega_parts(type1, known_thetas) - name <- parsed_raw$name - if (is.null(associated_theta)) { - associated_theta <- parsed_raw$associated_theta - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - parsed_raw$parameterization, - "OMEGA" - ) - } - } - } else { - # Omega style: name, theta_name, parameterization - if (is.null(name)) { - name <- type1$name - } - if (is.null(associated_theta)) { - associated_theta <- type1$theta_name - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - type1$parameterization, - "OMEGA" - ) - } - } - } - - # Fallback: extract from raw comment - if ( - (is.null(name) || is.null(associated_theta)) && - !is.null(raw) && - nzchar(raw) - ) { - parsed_raw <- extract_raw_omega_parts(raw, known_thetas) - if (is.null(name)) { - name <- parsed_raw$name - } - if (is.null(associated_theta)) { - associated_theta <- parsed_raw$associated_theta - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - parsed_raw$parameterization, - "OMEGA" - ) - } - } - - create_comment_with_sources( - OmegaComment, - omega_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - parameterization = parameterization, - associated_theta = associated_theta - ) -} - -#' @noRd -parse_type1_sigma_comment <- function( - nonmem_name, - name, - parsed, - raw, - mod_path -) { - name <- normalize_comment_name(name) - - unit <- NULL - parameterization <- NULL - - # Try to extract from parsed comment - if (!is.null(parsed) && !is.null(parsed$Type1)) { - type1 <- parsed$Type1 - - if (is.character(type1)) { - # Type1$Unknown: raw string stored directly - parsed_raw <- extract_raw_sigma_parts(type1) - if (is.null(name)) { - name <- parsed_raw$name - } - if (is.null(unit)) { - unit <- parsed_raw$unit - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - parsed_raw$parameterization, - "SIGMA" - ) - } - } else { - # Sigma style: name, parameterization - if (is.null(name)) { - name <- type1$name - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - type1$parameterization, - "SIGMA" - ) - } - } - } - - # Fallback: extract from raw comment - if (!is.null(raw) && nzchar(raw)) { - parsed_raw <- extract_raw_sigma_parts(raw) - if (is.null(name)) { - name <- parsed_raw$name - } - if (is.null(unit)) { - unit <- parsed_raw$unit - } - if (is.null(parameterization)) { - parameterization <- map_parameterization( - parsed_raw$parameterization, - "SIGMA" - ) - } - } - - create_comment_with_sources( - SigmaComment, - sigma_fields(), - mod_path, - nonmem_name = nonmem_name, - name = name, - unit = unit, - parameterization = parameterization - ) -} - -#' Extract name from raw comment string -#' -#' Finds the first alphanumeric word, skipping leading pure numbers. -#' -#' @param raw Character string of the raw comment -#' @return Character string of the extracted name, or NULL if none found -#' @noRd -extract_name_from_raw <- function(raw) { - if (is.null(raw) || !nzchar(trimws(raw))) { - return(NULL) - } - - words <- strsplit(trimws(raw), "\\s+")[[1]] - idx <- find_first_name_idx(words) - - if (!is.na(idx)) { - return(words[idx]) - } - - NULL -} - -#' Extract parameterization suffix from raw comment -#' -#' Handles formats: "; exp", ";exp", " :EXP" -#' -#' @param raw Character string of the raw comment -#' @return Named list with remaining raw string and parameterization -#' @noRd -extract_parameterization_suffix <- function(raw) { - parameterization <- NULL - - if (grepl(";", raw)) { - parts <- strsplit(raw, ";")[[1]] - raw <- trimws(parts[1]) - if (length(parts) >= 2) { - param_part <- trimws(parts[2]) - if (nzchar(param_part)) { - parameterization <- param_part - } - } - } else if (grepl("\\s+:", raw)) { - match <- regmatches(raw, regexec("\\s+:\\s*(.+)\\s*$", raw))[[1]] - if (length(match) >= 2) { - parameterization <- trimws(match[2]) - raw <- trimws(sub("\\s+:\\s*.+\\s*$", "", raw)) - } - } - - list(raw = raw, parameterization = parameterization) -} - -#' Strip parameter prefix from raw comment -#' -#' Removes THETAn:, OMEGAn:, OMEGA(n,n):, SIGMAn:, SIGMA(n,n): prefixes -#' -#' @param raw Character string of the raw comment -#' @return Character string with prefix removed -#' @noRd -strip_param_prefix <- function(raw) { - # Colon after parameter identifier is optional - raw <- gsub("^THETA\\(\\d+\\):?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^THETA\\d+:?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^OMEGA\\d+:?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^OMEGA\\(\\d+,\\d+\\):?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^SIGMA\\(\\d+\\):?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^SIGMA\\d+:?\\s*", "", raw, ignore.case = TRUE) - raw <- gsub("^SIGMA\\(\\d+,\\d+\\):?\\s*", "", raw, ignore.case = TRUE) - # Also handle bare number prefix like "1:", "1-", "1.", or "1 " - raw <- gsub("^\\d+[-:.]?\\s*", "", raw) - raw -} - -#' Find first word containing letters -#' -#' @param words Character vector of words -#' @return Index of first word with letters, or NA if none found -#' @noRd -find_first_name_idx <- function(words) { - for (i in seq_along(words)) { - if (grepl("[A-Za-z]", words[i])) { - return(i) - } - } - NA_integer_ -} - -#' Extract components from raw theta comment string -#' -#' Parses comments like "THETA1: CL (L/day) ; exp" or "CL (L/day)" -#' -#' @param raw Character string of the raw comment -#' @return Named list with name, unit, and parameterization -#' @noRd -extract_raw_theta_parts <- function(raw) { - result <- list(name = NULL, unit = NULL, parameterization = NULL) - - if (is.null(raw) || !nzchar(trimws(raw))) { - return(result) - } - - raw <- trimws(raw) - - # Extract parameterization suffix - extracted <- extract_parameterization_suffix(raw) - raw <- extracted$raw - result$parameterization <- extracted$parameterization - - # Strip parameter prefix - raw <- strip_param_prefix(raw) - - # Extract unit from parentheses or brackets (anywhere in the string) - unit_parts <- extract_unit_anywhere(raw) - raw <- unit_parts$raw - if (is.null(result$unit)) { - result$unit <- unit_parts$unit - } - - # Find name (first word with letters) - if (nzchar(raw)) { - words <- strsplit(raw, "\\s+")[[1]] - idx <- find_first_name_idx(words) - if (!is.na(idx)) { - # Strip trailing punctuation (comma, period, etc.) - result$name <- gsub("[,.:;]+$", "", words[idx]) - } - } - - result -} - -#' Check if a bracketed string looks like a unit -#' -#' @param value Character string inside () or [] -#' @return TRUE if value looks like a unit -#' @noRd -is_unit_like <- function(value) { - if (is.null(value) || !nzchar(trimws(value))) { - return(FALSE) - } - - value <- trimws(value) - - if (grepl(",", value, fixed = TRUE)) { - return(FALSE) - } - - # Allow alphabetic abbreviations like CONC or prop - if (grepl("^[A-Za-z0-9_]+$", value)) { - return(TRUE) - } - - # Allow unit-ish tokens that include separators or digits - grepl("[/0-9%^]", value) -} - -#' Find the matching closing delimiter for a balanced segment -#' -#' @param raw Character string to scan -#' @param open_pos Position of the opening delimiter -#' @param open_char Opening delimiter character -#' @param close_char Closing delimiter character -#' @return Integer position of matching closing delimiter, or NA -#' @noRd -find_balanced_close <- function(raw, open_pos, open_char, close_char) { - raw_len <- nchar(raw) - depth <- 0L - - for (i in seq.int(open_pos, raw_len)) { - char_i <- substr(raw, i, i) - if (identical(char_i, open_char)) { - depth <- depth + 1L - } else if (identical(char_i, close_char)) { - depth <- depth - 1L - if (depth == 0L) { - return(i) - } - } - } - - NA_integer_ -} - -#' Extract unit from parentheses or brackets anywhere in the string -#' -#' @param raw Character string of the raw comment -#' @return Named list with raw (unit removed) and unit -#' @noRd -extract_unit_anywhere <- function(raw) { - result <- list(raw = raw, unit = NULL) - - if (is.null(raw) || !nzchar(trimws(raw))) { - return(result) - } - - pos <- 1 - raw_len <- nchar(raw) - - while (pos <= raw_len) { - paren_start <- regexpr("(", substr(raw, pos, raw_len), fixed = TRUE)[1] - bracket_start <- regexpr("[", substr(raw, pos, raw_len), fixed = TRUE)[1] - - paren_pos <- if (paren_start == -1) Inf else pos + paren_start - 1 - bracket_pos <- if (bracket_start == -1) Inf else pos + bracket_start - 1 - - if (is.infinite(paren_pos) && is.infinite(bracket_pos)) { - return(result) - } - - if (paren_pos <= bracket_pos) { - close_pos <- find_balanced_close(raw, paren_pos, "(", ")") - if (is.na(close_pos)) { - return(result) - } - candidate <- substr(raw, paren_pos + 1, close_pos - 1) - if (is_unit_like(candidate)) { - result$unit <- candidate - result$raw <- trimws( - paste0( - substr(raw, 1, paren_pos - 1), - substr(raw, close_pos + 1, raw_len) - ) - ) - return(result) - } - pos <- close_pos + 1 - } else { - close_pos <- find_balanced_close(raw, bracket_pos, "[", "]") - if (is.na(close_pos)) { - return(result) - } - candidate <- substr(raw, bracket_pos + 1, close_pos - 1) - if (is_unit_like(candidate)) { - result$unit <- candidate - result$raw <- trimws( - paste0( - substr(raw, 1, bracket_pos - 1), - substr(raw, close_pos + 1, raw_len) - ) - ) - return(result) - } - pos <- close_pos + 1 - } - } - - result -} - -#' Split theta reference into associated thetas -#' -#' Splits on separators unless the string matches a known theta name (case-insensitive). -#' -#' @param theta_ref Character string of the theta reference -#' @param known_thetas Character vector of known theta names for context -#' @return Character vector of associated theta names -#' @noRd -split_theta_reference <- function(theta_ref, known_thetas = NULL) { - if (is.null(theta_ref) || !nzchar(theta_ref)) { - return(NULL) - } - - theta_ref <- trimws(theta_ref) - - # Check if it matches a known theta (case-insensitive) - if (!is.null(known_thetas) && length(known_thetas) > 0) { - if (tolower(theta_ref) %in% tolower(known_thetas)) { - return(theta_ref) - } - - # Preserve off-diagonal pairs like "CL/F-V2/F" when both parts - # are known theta names. - for (sep in c("-", ",", ":")) { - if (grepl(sep, theta_ref, fixed = TRUE)) { - parts <- trimws(strsplit(theta_ref, sep, fixed = TRUE)[[1]]) - if ( - length(parts) == 2 && - all(nzchar(parts)) && - all(tolower(parts) %in% tolower(known_thetas)) - ) { - return(parts) - } - } - } - } - - # Otherwise split on separators - if (grepl("[-/:,]", theta_ref)) { - parts <- strsplit(theta_ref, "[-/:,]")[[1]] - return(trimws(parts)) - } - - theta_ref -} - -#' Extract components from raw omega comment string -#' -#' Parses comments like "OM1 CL", "OM1 CL :EXP", "OMEGA1: CL ; exp", or "OM2,1 CL-VC". -#' Builds composite names (e.g., "IIV CL" -> "IIV-CL") and extracts associated thetas. -#' -#' @param raw Character string of the raw comment -#' @param known_thetas Character vector of known theta names for context-aware splitting -#' @return Named list with name, associated_theta (character vector), and parameterization -#' @noRd -extract_raw_omega_parts <- function(raw, known_thetas = NULL) { - result <- list(name = NULL, associated_theta = NULL, parameterization = NULL) - - if (is.null(raw) || !nzchar(trimws(raw))) { - return(result) - } - - raw <- trimws(raw) - - # Extract parameterization suffix - extracted <- extract_parameterization_suffix(raw) - raw <- extracted$raw - result$parameterization <- extracted$parameterization - - # Strip parameter prefix - raw <- strip_param_prefix(raw) - - # Split remaining into words and find first word with letters - - words <- strsplit(raw, "\\s+")[[1]] - idx <- find_first_name_idx(words) - - if (is.na(idx)) { - return(result) - } - - first_word <- words[idx] - prefix <- NULL - theta_ref <- NULL - - # First token may itself be an off-diagonal theta pair - # (e.g., "CL/F-V2/F", "CL/F:V2/F", or "CL/F,V2/F"). - pair <- split_theta_reference(first_word, known_thetas) - has_known_pair <- !is.null(known_thetas) && - length(known_thetas) > 0 && - length(pair) == 2 && - !any(is.na(pair)) && - all(tolower(pair) %in% tolower(known_thetas)) - - if (has_known_pair) { - result$associated_theta <- pair - } else if (grepl("-", first_word)) { - # Check if first word already contains a hyphen (e.g., "IIV-CL", "Corr-CL-V") - # Split on first hyphen only to get prefix - hyphen_pos <- regexpr("-", first_word) - prefix <- substr(first_word, 1, hyphen_pos - 1) - theta_ref <- substr(first_word, hyphen_pos + 1, nchar(first_word)) - } else { - # First word is the prefix, look for theta reference in subsequent words - prefix <- first_word - - # Find theta reference, skipping linking words like "on", "for" - linking_words <- c("on", "for", "of") - theta_idx <- idx + 1 - while ( - theta_idx <= length(words) && - tolower(words[theta_idx]) %in% linking_words - ) { - theta_idx <- theta_idx + 1 - } - - if (theta_idx <= length(words)) { - theta_ref <- words[theta_idx] - } - } - - # Store prefix as name, theta reference separately in associated_theta - result$name <- prefix - if ( - is.null(result$associated_theta) && !is.null(theta_ref) && nzchar(theta_ref) - ) { - result$associated_theta <- split_theta_reference(theta_ref, known_thetas) - } - - result -} - -#' Extract components from raw sigma comment string -#' -#' Parses comments like "SIG1", "PropErr", "AddErr (ng/mL) :PROP", -#' or "SIGMA1: PropErr ; prop" -#' -#' @param raw Character string of the raw comment -#' @return Named list with name, unit, and parameterization -#' @noRd -extract_raw_sigma_parts <- function(raw) { - result <- list(name = NULL, unit = NULL, parameterization = NULL) - - if (is.null(raw) || !nzchar(trimws(raw))) { - return(result) - } - - raw <- trimws(raw) - - # Extract parameterization suffix using shared helper - extracted <- extract_parameterization_suffix(raw) - raw <- extracted$raw - result$parameterization <- extracted$parameterization - - if (!is.null(result$parameterization) && nzchar(result$parameterization)) { - unit_parts <- extract_unit_anywhere(result$parameterization) - if (!is.null(unit_parts$unit)) { - result$unit <- unit_parts$unit - result$parameterization <- unit_parts$raw - } - } - - # Strip parameter prefix using shared helper - raw <- strip_param_prefix(raw) - - # Extract unit from parentheses or brackets (anywhere in the string) - unit_parts <- extract_unit_anywhere(raw) - raw <- unit_parts$raw - if (is.null(result$unit)) { - result$unit <- unit_parts$unit - } - - # Find name (first word with letters) using shared helper - words <- strsplit(raw, "\\s+")[[1]] - idx <- find_first_name_idx(words) - if (!is.na(idx)) { - result$name <- words[idx] - } - - result -} diff --git a/R/comments-query.R b/R/comments-query.R index 0a349efe..828ced1d 100644 --- a/R/comments-query.R +++ b/R/comments-query.R @@ -37,41 +37,19 @@ get_comment <- function(model_comments, nonmem_name) { NULL } +#' Check if an omega parameter is diagonal (variance) vs off-diagonal (covariance) #' @noRd -build_comment_lookup <- function(model_comments) { - all_comments <- c( - model_comments@theta, - model_comments@omega, - model_comments@sigma - ) - - by_user_name <- list() - comments_by_kind <- list( - THETA = model_comments@theta, - OMEGA = model_comments@omega, - SIGMA = model_comments@sigma - ) - for (kind in names(comments_by_kind)) { - for (comment in comments_by_kind[[kind]]) { - if (!is.null(comment@name)) { - if (is.null(by_user_name[[comment@name]])) { - by_user_name[[comment@name]] <- comment - } else { - rlang::warn( - paste0( - "Duplicate parameter name '", - comment@name, - "' across parameter kinds; using first occurrence (", - kind, - ")." - ) - ) - } - } - } +is_diagonal_omega <- function(nonmem_name) { + # Parse OMEGA(i,j) format + match <- regmatches( + nonmem_name, + regexec("OMEGA\\((\\d+),(\\d+)\\)", nonmem_name) + )[[1]] + if (length(match) == 3) { + return(match[2] == match[3]) } - - list(by_nonmem_name = all_comments, by_user_name = by_user_name) + # If we can't parse, assume diagonal + TRUE } #' @noRd @@ -88,6 +66,13 @@ resolve_comment <- function(model_comments, nm, kind = NULL) { if (!is.null(cmt@name) && identical(cmt@name, lookup_nm)) { return(cmt) } + if ( + S7::S7_inherits(cmt, OmegaComment) && + !is.null(cmt@raw_name) && + identical(cmt@raw_name, lookup_nm) + ) { + return(cmt) + } } NULL } @@ -256,25 +241,9 @@ get_parameter_names <- function(x, lookup_path = NULL) { } model_comments <- x - extract_row <- function(comment, include_associated_theta = FALSE) { - name_val <- comment@name %||% NA_character_ - # For omega: build composite name with associated_theta (avoiding duplicates) - if ( - include_associated_theta && - !is.null(comment@associated_theta) && - length(comment@associated_theta) > 0 - ) { - if (is.na(name_val)) { - name_val <- paste(comment@associated_theta, collapse = ", ") - } else { - name_val <- format_omega_display_name( - name_val, - comment@associated_theta - ) - } - } + extract_row <- function(comment) { data.frame( - name = name_val, + name = comment@name %||% NA_character_, display = comment@display %||% NA_character_, stringsAsFactors = FALSE ) @@ -289,13 +258,7 @@ get_parameter_names <- function(x, lookup_path = NULL) { } for (nm in names(model_comments@omega)) { - rows <- c( - rows, - list(extract_row( - model_comments@omega[[nm]], - include_associated_theta = TRUE - )) - ) + rows <- c(rows, list(extract_row(model_comments@omega[[nm]]))) row_names <- c(row_names, nm) } diff --git a/R/comments-utils.R b/R/comments-utils.R index c8dd7cb4..b94ac4e2 100644 --- a/R/comments-utils.R +++ b/R/comments-utils.R @@ -1,142 +1,3 @@ -#' Format omega display name, avoiding duplicate theta info -#' -#' Builds a display name for omega parameters by appending associated theta -#' information, but only if that information isn't already present in the name. -#' This prevents duplication like "IIV-CL (CL)" when the omega was already -#' renamed to include the theta. -#' -#' @param name The omega parameter name (e.g., "IIV-CL" or "IIV") -#' @param associated_theta Character vector of associated theta names -#' @param theta_labels Optional named vector mapping theta names to display -#' labels. If provided, uses labels for the suffix; otherwise uses theta names. -#' -#' @return The formatted display name with theta info appended only if missing -#' -#' @examples -#' # Theta already in name - no duplication -#' format_omega_display_name("IIV-CL", "CL") -#' # Returns: "IIV-CL" -#' -#' # Theta not in name - appends it -#' format_omega_display_name("IIV", "CL") -#' # Returns: "IIV CL" -#' -#' # Multiple thetas -#' format_omega_display_name("IIV", c("CL", "V")) -#' # Returns: "IIV CL, V" -#' -#' # With custom labels -#' format_omega_display_name("IIV", "CL", c(CL = "Clearance")) -#' # Returns: "IIV Clearance" -#' -#' @keywords internal -#' @export -format_omega_display_name <- function( - name, - associated_theta, - theta_labels = NULL -) { - if (is.null(associated_theta) || length(associated_theta) == 0) { - return(name) - } - - # Determine what labels to use for checking and appending - if (!is.null(theta_labels)) { - labels_to_use <- vapply( - associated_theta, - function(theta) { - if (theta %in% names(theta_labels)) { - theta_labels[[theta]] - } else { - theta - } - }, - character(1) - ) - } else { - labels_to_use <- associated_theta - } - - # Extract root for display (strip prefixes/suffixes, preserve case) - extract_root <- function(term) { - # Strip TV/ETA prefix - term <- sub("^(TV|ETA)", "", term, ignore.case = TRUE) - # Strip / suffix (e.g., /F) - sub("/[A-Za-z]$", "", term) - } - - # Normalize for matching (root + lowercase) - normalize_for_match <- function(term) { - tolower(extract_root(term)) - } - - # Normalize into "token space" form for phrase matching - # Keeps / as part of tokens, converts other non-alphanumeric to spaces - normalize_for_phrase <- function(x) { - x <- tolower(x) - x <- gsub("[^a-z0-9/]+", " ", x) - x <- gsub("\\s+", " ", x) - trimws(x) - } - - # Prepare padded omega name for phrase-safe matching - omega_phrase_normalized <- normalize_for_phrase(name) - omega_padded <- paste0(" ", omega_phrase_normalized, " ") - - # Split omega name into segments on hyphen and space (preserve / within segments) - omega_segments_raw <- unlist(strsplit(name, "[- ]+")) - omega_segments_normalized <- vapply( - omega_segments_raw, - normalize_for_match, - character(1) - ) - - # Check which thetas are already present in the name - theta_already_present <- vapply( - seq_along(associated_theta), - function(i) { - theta <- associated_theta[i] - label <- labels_to_use[i] - - # Normalize theta and label for comparison - theta_normalized <- normalize_for_match(theta) - label_normalized <- normalize_for_match(label) - - # Phrase-safe checks using padded boundaries - # Handles multi-word labels like "CL/F Scaling" without matching substrings - label_phrase <- normalize_for_phrase(label) - theta_phrase <- normalize_for_phrase(theta) - - if (grepl(paste0(" ", label_phrase, " "), omega_padded, fixed = TRUE)) { - return(TRUE) - } - if (grepl(paste0(" ", theta_phrase, " "), omega_padded, fixed = TRUE)) { - return(TRUE) - } - - # Fall back to segment-based matching - if (theta_normalized %in% omega_segments_normalized) { - return(TRUE) - } - if (label_normalized %in% omega_segments_normalized) { - return(TRUE) - } - - FALSE - }, - logical(1) - ) - - # Only append missing thetas (keep original name for display) - missing_labels <- labels_to_use[!theta_already_present] - if (length(missing_labels) > 0) { - theta_str <- paste(missing_labels, collapse = ", ") - paste0(name, " ", theta_str) - } else { - name - } -} - #' Convert comment list to data frame with values #' @param comments Named list of comment objects #' @param fields Character vector of field names to extract @@ -187,3 +48,38 @@ build_comment_tables <- function(comments_list, fields_list, value_resolver) { } tables } + +#' Make a path relative to project root (pharos.toml directory) +#' @noRd +relative_path <- function(path) { + if (is.null(path) || path == "default" || path == "user supplied") { + return(path) + } + tryCatch(to_config_relative(path), error = function(e) path) +} + +#' Set source paths for comment fields +#' +#' Always initializes the sources attribute to mark object as "initialized". +#' Fields with non-NULL values get source_path; NULL fields get "default". +#' @noRd +set_sources <- function(comment, fields, source_path) { + source_path <- relative_path(source_path) + sources <- list() + for (f in fields) { + val <- S7::prop(comment, f) + if (!is.null(val)) { + sources[[f]] <- source_path + } else { + sources[[f]] <- "default" + } + } + attr(comment, "sources") <- sources + comment +} + +#' @noRd +create_comment_with_sources <- function(constructor, fields, mod_path, ...) { + comment <- constructor(...) + set_sources(comment, fields, mod_path) +} diff --git a/R/dataset.R b/R/dataset.R index fc45fe94..f32f18eb 100644 --- a/R/dataset.R +++ b/R/dataset.R @@ -3,7 +3,7 @@ #' @param x A hyperion_nonmem_dataset object #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_nonmem_dataset) +#' @exportS3Method base::print hyperion_nonmem_dataset print.hyperion_nonmem_dataset <- function(x, ...) { rel_path <- to_config_relative(x$canonical_path) diff --git a/R/extendr-wrappers.R b/R/extendr-wrappers.R index 6acc85f0..05fed1f3 100644 --- a/R/extendr-wrappers.R +++ b/R/extendr-wrappers.R @@ -18,15 +18,18 @@ NULL #' } init <- function(config_path) .Call(wrap__init, config_path) -set_panic_message <- function() .Call(wrap__set_panic_message) +silence_panic_output <- function() .Call(wrap__silence_panic_output) find_pharos_config_file <- function() .Call(wrap__find_pharos_config_file) -#' Gets model object +#' Read a NONMEM model from a .mod or .ctl file #' -#' @param path path to mod or ctl file. +#' @param path path to a .mod or .ctl file. #' -#' @return hyperion_nonmem_model S3 object with `model_source` and `run_status` attributes +#' @return A `hyperion_nonmem_model` S3 object with attributes: +#' - `filename`: the model stem (e.g. `"run001"`) +#' - `model_source`: path to the source file, relative to the pharos config dir +#' - `run_status`: `"run"`, `"running"`, or `"not_run"` determined from output files on disk #' @export #' #' @examples \dontrun{ @@ -34,24 +37,14 @@ find_pharos_config_file <- function() .Call(wrap__find_pharos_config_file) #' } read_model <- function(path) .Call(wrap__read_model, path) -#' Checks model dataset -#' -#' @param model hyperion_nonmem_model object from `read_model` -#' -#' @return Dataset check results -#' @export -#' -#' @examples \dontrun{ -#' model <- read_model("model/nonmem/run001.mod") -#' model |> check_dataset() -#' } -check_dataset <- function(model) .Call(wrap__check_dataset, model) - -#' Gets model object from lst file (internal) +#' Read a model from an .lst file (internal) #' -#' @param path path to lst file, model output directory, or metadata.json file. +#' @param path path to an .lst file, model output directory, or metadata.json file. #' -#' @return hyperion_nonmem_model S3 object with `model_source` attribute for the source file +#' @return A `hyperion_nonmem_model` S3 object with attributes: +#' - `filename`: the model stem (e.g. `"run001"`) +#' - `model_source`: path to the source file, relative to the pharos config dir +#' - `run_status`: `"run"`, `"running"`, or `"not_run"` determined from output files on disk #' @keywords internal read_model_from_lst <- function(path) .Call(wrap__read_model_from_lst, path) @@ -70,6 +63,8 @@ read_model_from_lst <- function(path) .Call(wrap__read_model_from_lst, path) #' Examples: "THETA1" or c("THETA1") #' @param seed integer for random number generator seed to ensure reproducible jittering #' @param description Description of model in metadata file +#' @param based_on Character vector of model names/paths that this model is based on +#' @param tags Character vector of tags to attach to the model in metadata #' @param no_metadata boolean, if true, does not create metadatafile, default FALSE #' #' @return path to new model file (invisible) todo @@ -78,7 +73,7 @@ read_model_from_lst <- function(path) .Call(wrap__read_model_from_lst, path) #' @examples \dontrun{ #' copy_model(from = "model/nonmem/run001.mod", to = "model/nonmem/run002.mod") #' } -copy_model <- function(from, to, overwrite = FALSE, ext_file = NULL, update = 'none', jitter = NULL, jitter_excluded = NULL, seed = NULL, description = NULL, no_metadata = FALSE) .Call(wrap__copy_model_wrap, from, to, overwrite, ext_file, update, jitter, jitter_excluded, seed, description, no_metadata) +copy_model <- function(from, to, overwrite = FALSE, ext_file = NULL, update = 'none', jitter = NULL, jitter_excluded = NULL, seed = NULL, description, based_on = NULL, tags = NULL, no_metadata = FALSE) .Call(wrap__copy_model_wrap, from, to, overwrite, ext_file, update, jitter, jitter_excluded, seed, description, based_on, tags, no_metadata) #' Gets model run summary (internal implementation) #' @@ -120,20 +115,42 @@ get_run_info <- function(path) .Call(wrap__get_run_info, path) #' } check_model <- function(model_path) .Call(wrap__check_model_wrap, model_path) -#' Get's model lineage +#' Checks model dataset +#' +#' @param model hyperion_nonmem_model object from `read_model` +#' +#' @return Dataset check results +#' @export +check_dataset <- function(model) .Call(wrap__check_dataset, model) + +#' Show model lineage and relationships. +#' +#' With no arguments, returns the full project lineage tree. Supplying a +#' model path returns that model's full lineage (ancestors and descendants). +#' The `from` and `to` arguments filter the tree from a model downward, up +#' to a model, or to the slice between two models. The project is always +#' rooted at the directory containing `pharos.toml`. #' -#' @param model_dir path to directory containing all models, or a hyperion_nonmem_model object -#' (uses the model's parent directory) +#' @param model Optional `hyperion_nonmem_model` object or model file path. +#' Returns the model's full lineage (ancestors and descendants). Conflicts +#' with `from`/`to`. +#' @param from Filter the tree to this model and everything downstream. +#' Accepts a `hyperion_nonmem_model` object or a model file path. +#' @param to Filter the tree to this model and everything upstream. +#' Accepts a `hyperion_nonmem_model` object or a model file path. #' #' @return hyperion_nonmem_tree S3 object #' @export #' #' @examples \dontrun{ -#' get_model_lineage("model/nonmem/") -#' model <- read_model("model/nonmem/run001.mod") -#' get_model_lineage(model) +#' get_model_lineage() # whole project +#' get_model_lineage("model/nonmem/run003.mod") # full lineage of run003 +#' get_model_lineage(from = "model/nonmem/run001.mod") # run001 and descendants +#' get_model_lineage(to = "model/nonmem/run003.mod") # run003 and ancestors +#' get_model_lineage(from = "model/nonmem/run001.mod", +#' to = "model/nonmem/run003.mod") # slice between two models #' } -get_model_lineage <- function(model_dir) .Call(wrap__get_model_lineage, model_dir) +get_model_lineage <- function(model = NULL, from = NULL, to = NULL) .Call(wrap__get_model_lineage, model, from, to) #' Gets parameter estimates from model run #' @@ -165,10 +182,27 @@ get_parameters <- function(path, hide_off_diagonal_params = FALSE, only_method = #' #' @param model hyperion_nonmem_model object from read_model() #' -#' @return Named character vector with NONMEM names as names and user-friendly names as values +#' @return Named list with NONMEM names as names and user-friendly names as character values #' @keywords internal get_model_parameter_names <- function(model) .Call(wrap__get_model_parameter_names, model) +#' Build per-parameter comment info from a model object (internal) +#' +#' @param model hyperion_nonmem_model object from read_model() +#' +#' @return list with `thetas`, `omegas`, `sigmas` entries; each is a list of +#' length-2 lists `(coordinate, info)` in numeric coordinate order. +#' @keywords internal +get_model_comment_info <- function(model) .Call(wrap__get_model_comment_info, model) + +#' Canonicalize a parameterization alias to its PascalCase form. +#' +#' @param raw Parameterization alias (e.g. `"EXP"`, `"lognormal"`, `"PROP"`). +#' @return Canonical name (`"LogNormal"`, `"Proportional"`, ...) or `NA_character_` +#' if `raw` is not a recognized alias. +#' @keywords internal +map_parameterization <- function(raw) .Call(wrap__map_parameterization, raw) + #' Creates a metadata file for a NONMEM model #' #' This function creates a metadata file that stores information about a NONMEM model, @@ -179,6 +213,7 @@ get_model_parameter_names <- function(model) .Call(wrap__get_model_parameter_nam #' @param description Optional description of the model and its purpose #' @param tags Character vector of tags to categorize or label the model #' @param based_on Character vector of model names/paths that this model is based on +#' @param copied_from Optional model name/path this model was mechanically copied from #' #' @return Returns invisibly after creating the metadata file #' @export @@ -200,7 +235,7 @@ get_model_parameter_names <- function(model) .Call(wrap__get_model_parameter_nam #' based_on = c("run001.mod") #' ) #' } -set_metadata_file <- function(model_path, description = NULL, tags = NULL, based_on = NULL) .Call(wrap__set_metadata_file, model_path, description, tags, based_on) +set_metadata_file <- function(model_path, description = NULL, tags = NULL, based_on = NULL, copied_from = NULL) .Call(wrap__set_metadata_file, model_path, description, tags, based_on, copied_from) #' Updates a metadatafile #' @@ -244,6 +279,27 @@ update_metadata_file <- function(model_path, description = NULL, tags = NULL, ba #' } get_model_metadata <- function(model) .Call(wrap__load_model_metadata, model) +#' Clear fields in a model's metadata file +#' +#' Selectively clears the `based_on`, `copied_from`, and/or `tags` fields in +#' the metadata file associated with a model. Fields not selected are left +#' unchanged. +#' +#' @param model_path Path to the NONMEM model file, or a hyperion_nonmem_model object +#' @param based_on If TRUE, clear the based_on field. Default FALSE. +#' @param copied_from If TRUE, clear the copied_from field. Default FALSE. +#' @param tags If TRUE, clear the tags field. Default FALSE. +#' +#' @return Returns invisibly after updating the metadata file +#' @export +#' +#' @examples \dontrun{ +#' clear_metadata_file("model/nonmem/run001.mod", tags = TRUE) +#' model <- read_model("model/nonmem/run001.mod") +#' clear_metadata_file(model, based_on = TRUE, copied_from = TRUE) +#' } +clear_metadata_file <- function(model_path, based_on = FALSE, copied_from = FALSE, tags = FALSE) .Call(wrap__clear_metadata_file_wrap, model_path, based_on, copied_from, tags) + #' Determine run status for a model path, run directory, or model object. #' #' @param input A hyperion_nonmem_model object, run directory, or model path. diff --git a/R/hyperion-package.R b/R/hyperion-package.R index b9df26df..f91b7d11 100644 --- a/R/hyperion-package.R +++ b/R/hyperion-package.R @@ -84,8 +84,8 @@ #' \itemize{ #' \item [init()] - Initialize pharos with config file path #' \item [get_pharos_config()] - Get current pharos configuration -#' \item [get_comment_type()] - Get comment parsing mode (raw or type1) -#' \item [use_type1_comments()] - Configure pharos.toml for type1 comment parsing +#' \item [get_comment_type()] - Get comment parsing mode (type1 or type2) +#' \item [use_comments()] - Configure pharos.toml comment parsing type #' } #' #' @section Metadata Files: diff --git a/R/metadata-methods.R b/R/metadata-methods.R index cd37dc57..4fe875fb 100644 --- a/R/metadata-methods.R +++ b/R/metadata-methods.R @@ -3,7 +3,7 @@ #' @param x A hyperion_model_metadata object #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_model_metadata) +#' @exportS3Method base::print hyperion_model_metadata print.hyperion_model_metadata <- function(x, ...) { description <- x$description %||% "" tags <- x$tags %||% character(0) diff --git a/R/model-methods.R b/R/model-methods.R index 1e681f64..c9274907 100644 --- a/R/model-methods.R +++ b/R/model-methods.R @@ -4,7 +4,7 @@ #' @param digits Number of significant digits (uses global option if NULL) #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_nonmem_model) +#' @exportS3Method base::print hyperion_nonmem_model print.hyperion_nonmem_model <- function(x, digits = NULL, ...) { parts <- build_model_display_parts(x, digits) @@ -18,18 +18,6 @@ print.hyperion_nonmem_model <- function(x, digits = NULL, ...) { cli::cli_text("{.strong Run Status:} {parts$run_status}") } - if (!is.null(parts$records)) { - cli::cli_text("{.strong Records:} {parts$records$count} record blocks") - if (length(parts$records$types) > 0) { - cli::cli_text("{.strong Record Types:}") - for (i in seq_along(parts$records$types)) { - type <- names(parts$records$types)[i] - count <- parts$records$types[i] - cli::cli_text(" \u2022 {type}: {count}") - } - } - } - if (!is.null(parts$data$dataset)) { cli::cli_text("{.strong Dataset:} {parts$data$dataset}") } @@ -90,7 +78,7 @@ print.hyperion_nonmem_model <- function(x, digits = NULL, ...) { #' Default is 10. #' @param ... Additional arguments (currently unused) #' @return A hyperion_nonmem_summary object -#' @rawNamespace S3method(base::summary, hyperion_nonmem_model) +#' @exportS3Method base::summary hyperion_nonmem_model summary.hyperion_nonmem_model <- function( object, hide_off_diagonal_params = FALSE, @@ -101,57 +89,24 @@ summary.hyperion_nonmem_model <- function( run_status <- refresh_run_status(object) - # Handle "not_run" status - return informative summary instead of error - if (identical(run_status, "not_run")) { - return(build_not_run_summary(object)) - } - - if (identical(run_status, "running")) { - return(build_running_summary(object, n_iterations)) - } - - if (!identical(run_status, "run")) { + result <- if (identical(run_status, "not_run")) { + build_not_run_summary(object) + } else if (identical(run_status, "running")) { + build_running_summary(object, n_iterations) + } else if (identical(run_status, "run")) { + get_model_summary( + object, + hide_off_diagonal_params = hide_off_diagonal_params + ) + } else { rlang::abort(paste0( "model run_status must be 'run', 'running', or 'not_run', got: ", run_status )) } - summary_obj <- get_model_summary( - object, - hide_off_diagonal_params = hide_off_diagonal_params - ) - - comment_type <- get_comment_type() - is_type1 <- !is.null(comment_type) && - is.character(comment_type) && - length(comment_type) == 1 && - identical(tolower(comment_type), "type1") - - if (!is_type1 && !is.null(summary_obj$parameters)) { - tryCatch( - { - info <- get_model_parameter_info(object) - name_map <- get_parameter_names(info) - - if (nrow(name_map) > 0 && "name" %in% names(summary_obj$parameters)) { - nonmem_names <- summary_obj$parameters$name - mapped <- name_map[nonmem_names, "name", drop = TRUE] - replace_idx <- !is.na(mapped) & nzchar(mapped) - summary_obj$parameters$name[replace_idx] <- mapped[replace_idx] - } - }, - error = function(e) { - rlang::warn(c( - "Could not apply parameter names from model comments.", - "i" = "Falling back to NONMEM parameter names.", - "x" = conditionMessage(e) - )) - } - ) - } - - summary_obj + result$model_file <- from_config_relative(attr(object, "model_source")) + result } #' Build summary object for a running model @@ -166,8 +121,8 @@ build_running_summary <- function(object, n_iterations) { model_path <- from_config_relative(attr(object, "model_source")) # Derive expected output file paths to check existence before calling Rust. - # Rust panics print error messages via the panic hook before tryCatch can - # suppress them, so we must avoid calling Rust when files don't exist. + # Rust panics abort the R session; tryCatch cannot recover, so guard with + # file.exists() before calling Rust on missing files. base_name <- tools::file_path_sans_ext(basename(model_path)) output_dir <- file.path(dirname(model_path), base_name) @@ -178,7 +133,7 @@ build_running_summary <- function(object, n_iterations) { iterations <- tryCatch( { ext_data <- read_ext_file( - model_path, + object, parameters_only = TRUE, only_last = TRUE ) @@ -240,8 +195,8 @@ build_not_run_summary <- function(object) { if (!is.null(object$problem)) { if (is.character(object$problem) && length(object$problem) > 0) { problem <- object$problem - } else if (is.list(object$problem) && !is.null(object$problem$title)) { - problem <- object$problem$title + } else if (is.list(object$problem) && !is.null(object$problem$text)) { + problem <- object$problem$text } } @@ -297,11 +252,10 @@ validate_n_iterations <- function(n_iterations) { #' @param object A hyperion_nonmem_model object #' @param ... Additional arguments passed to str #' @return Invisible NULL (called for side effects) -#' @rawNamespace S3method(utils::str, hyperion_nonmem_model) +#' @exportS3Method utils::str hyperion_nonmem_model str.hyperion_nonmem_model <- function(object, ...) { class(object) <- "list" - object$tokens <- NULL - object$token_ranges <- NULL + object$source <- NULL utils::str(object, ...) } @@ -312,26 +266,26 @@ str.hyperion_nonmem_model <- function(object, ...) { #' @param x A hyperion_nonmem_model object #' @param name The element name to access #' @return The element value, or NULL for restricted fields -#' @rawNamespace S3method(base::`$`, hyperion_nonmem_model) +#' @exportS3Method base::`$` hyperion_nonmem_model `$.hyperion_nonmem_model` <- function(x, name) { - if (name %in% c("tokens", "token_ranges")) { + if (name %in% c("source")) { return(NULL) } .subset2(x, name) } -#' @rawNamespace S3method(base::`[[`, hyperion_nonmem_model) +#' @exportS3Method base::`[[` hyperion_nonmem_model `[[.hyperion_nonmem_model` <- function(x, i, ...) { - if (is.character(i) && i %in% c("tokens", "token_ranges")) { + if (is.character(i) && i %in% c("source")) { return(NULL) } NextMethod("[[") } -#' @rawNamespace S3method(base::names, hyperion_nonmem_model) +#' @exportS3Method base::names hyperion_nonmem_model names.hyperion_nonmem_model <- function(x) { n <- NextMethod("names") - setdiff(n, c("tokens", "token_ranges")) + setdiff(n, c("source")) } #' @noRd @@ -347,32 +301,13 @@ build_model_display_parts <- function(x, digits = NULL) { if (!is.null(x$problem)) { if (is.character(x$problem) && length(x$problem) > 0) { problem <- x$problem - } else if (is.list(x$problem) && !is.null(x$problem$title)) { - problem <- x$problem$title + } else if (is.list(x$problem) && !is.null(x$problem$text)) { + problem <- x$problem$text } } run_status <- format_run_status(refresh_run_status(x)) - records <- NULL - if (!is.null(x$records)) { - if (length(x$records) > 0) { - record_types <- sapply(x$records, function(r) { - if (is.list(r) && !is.null(r$record_type)) { - r$record_type - } else { - NA_character_ - } - }) - } else { - record_types <- character(0) - } - records <- list( - count = length(x$records), - types = table(record_types) - ) - } - data_info <- list(dataset = NULL, ignore = NULL, num_records = NULL) if (!is.null(x$data)) { if (is.character(x$data) && length(x$data) > 0) { @@ -401,16 +336,17 @@ build_model_display_parts <- function(x, digits = NULL) { ) for (col in x$input_columns) { - if (!is.null(col$Included)) { - included_cols <- c(included_cols, col$Included) - } else if (!is.null(col$Dropped)) { - dropped_cols <- c(dropped_cols, col$Dropped) - } else if (!is.null(col$Aliased)) { + kind <- col$kind + if (!is.null(kind$Included)) { + included_cols <- c(included_cols, kind$Included) + } else if (!is.null(kind$Dropped)) { + dropped_cols <- c(dropped_cols, kind$Dropped) + } else if (!is.null(kind$Aliased)) { aliased_cols <- rbind( aliased_cols, data.frame( - from = col$Aliased$from, - to = col$Aliased$to, + from = kind$Aliased$from, + to = kind$Aliased$to, stringsAsFactors = FALSE ) ) @@ -458,7 +394,6 @@ build_model_display_parts <- function(x, digits = NULL) { title = title, problem = problem, run_status = run_status, - records = records, data = data_info, input_columns = input_columns, tables = tables @@ -488,27 +423,50 @@ format_run_status <- function(run_status) { create_blocksame_data <- function(param_names, prev_block) { # BlockSame copies everything from the previous Block's parameters # but uses new parameter names (e.g., OMEGA(8,8) instead of OMEGA(7,7)) + num_params <- length(param_names) data.frame( Parameter = param_names, Initial = sapply( prev_block$parameters, - function(p) p$initial_value %||% NA + function(p) p$value %||% NA ), - Lower = sapply(prev_block$parameters, function(p) p$lower_bound %||% NA), - Upper = sapply(prev_block$parameters, function(p) p$upper_bound %||% NA), - Fixed = sapply( - prev_block$parameters, - function(p) ifelse(p$is_fixed %||% FALSE, "Yes", "No") + Fixed = rep( + ifelse(prev_block$fixed %||% FALSE, "Yes", "No"), + num_params ), Parametrization = rep( - prev_block$parametrization %||% "", - length(param_names) + format_parametrization(prev_block$parametrization), + num_params ), Comment = sapply(prev_block$parameters, function(p) p$comment %||% ""), stringsAsFactors = FALSE ) } +#' Format a Parametrization enum value for display +#' @noRd +format_parametrization <- function(param) { + if (is.null(param)) { + return("") + } + if (identical(param, "Cholesky")) { + return("Cholesky") + } + if (is.list(param) && !is.null(param$Axes)) { + parts <- c() + diag <- param$Axes$diagonal + off_diag <- param$Axes$off_diagonal + if (!is.null(diag)) { + parts <- c(parts, diag) + } + if (!is.null(off_diag)) { + parts <- c(parts, off_diag) + } + return(paste(parts, collapse = " ")) + } + "" +} + #' Get formatted theta parameter data (shared by console and knit functions) #' @@ -519,21 +477,21 @@ create_blocksame_data <- function(param_names, prev_block) { #' @keywords internal #' @noRd get_theta_parameter_data <- function(x, digits = NULL, theta_names) { - if (is.null(x$theta_parameters) || length(x$theta_parameters) == 0) { + if (is.null(x$thetas) || length(x$thetas) == 0) { return(NULL) } # Build parameter table param_data <- data.frame( Parameter = theta_names, - Initial = sapply(x$theta_parameters, function(p) p$initial_value %||% NA), - Lower = sapply(x$theta_parameters, function(p) p$lower_bound %||% NA), - Upper = sapply(x$theta_parameters, function(p) p$upper_bound %||% NA), + Initial = sapply(x$thetas, function(p) p$init %||% NA), + Lower = sapply(x$thetas, function(p) p$lower %||% NA), + Upper = sapply(x$thetas, function(p) p$upper %||% NA), Fixed = sapply( - x$theta_parameters, - function(p) ifelse(p$is_fixed %||% FALSE, "Yes", "No") + x$thetas, + function(p) ifelse(p$fixed %||% FALSE, "Yes", "No") ), - Comment = sapply(x$theta_parameters, function(p) p$comment %||% ""), + Comment = sapply(x$thetas, function(p) p$comment %||% ""), stringsAsFactors = FALSE ) @@ -570,14 +528,15 @@ get_random_effect_parameter_data <- function( block_data <- data.frame( Parameter = param_names[param_idx:(param_idx + num_params - 1)], - Initial = sapply(block$parameters, function(p) p$initial_value %||% NA), - Lower = sapply(block$parameters, function(p) p$lower_bound %||% NA), - Upper = sapply(block$parameters, function(p) p$upper_bound %||% NA), - Fixed = sapply( - block$parameters, - function(p) ifelse(p$is_fixed %||% FALSE, "Yes", "No") + Initial = sapply(block$parameters, function(p) p$value %||% NA), + Fixed = rep( + ifelse(block$fixed %||% FALSE, "Yes", "No"), + num_params + ), + Parametrization = rep( + format_parametrization(block$parametrization), + num_params ), - Parametrization = rep(block$parametrization %||% "", num_params), Comment = sapply(block$parameters, function(p) p$comment %||% ""), stringsAsFactors = FALSE ) @@ -659,16 +618,25 @@ format_ignore_condition <- function(ignore_obj) { # Format ValueFilter as field.op.value (e.g., "AN01FL.EQ.0") field <- ignore_obj$ValueFilter$field %||% NA_character_ op <- ignore_obj$ValueFilter$op %||% NA_character_ - value <- ignore_obj$ValueFilter$value %||% NA_character_ + + # Unwrap DataValueFilterKind enum (Number(f64) or String(String)) + raw_value <- ignore_obj$ValueFilter$value + if (is.list(raw_value)) { + value <- raw_value$Number %||% raw_value$String %||% NA_character_ + } else { + value <- raw_value %||% NA_character_ + } # Convert operation names to NONMEM-style operators op_map <- c( "Equal" = "EQ", "NotEqual" = "NE", "Greater" = "GT", - "GreaterEqual" = "GE", - "Less" = "LT", - "LessEqual" = "LE" + "GreaterOrEqual" = "GE", + "Lower" = "LT", + "LowerOrEqual" = "LE", + "EqualNumeric" = "EQN", + "NotEqualNumeric" = "NEN" ) op_symbol <- op_map[op] if (is.na(op_symbol) || is.null(op_symbol)) { @@ -713,26 +681,6 @@ knit_print.hyperion_nonmem_model <- function(x, ...) { ) } - if (!is.null(parts$records)) { - output <- c( - output, - paste0( - "Records: ", - parts$records$count, - " record blocks" - ) - ) - if (length(parts$records$types) > 0) { - output <- c(output, "Record Types:") - for (i in seq_along(parts$records$types)) { - type <- names(parts$records$types)[i] - count <- parts$records$types[i] - output <- c(output, paste0("- ", type, ": ", count)) - } - } - } - output <- c(output, "") - if (!is.null(parts$data$dataset)) { output <- c( output, diff --git a/R/parameter-info.R b/R/parameter-info.R new file mode 100644 index 00000000..21485808 --- /dev/null +++ b/R/parameter-info.R @@ -0,0 +1,157 @@ +#' Find and read model from .lst file in a directory +#' @noRd +read_model_from_lst_dir <- function(dir_path) { + lst_candidates <- list.files( + dir_path, + pattern = "\\.lst$", + ignore.case = TRUE, + full.names = TRUE + ) + if (length(lst_candidates) == 0) { + rlang::abort(paste0("lst file not found in run directory: ", dir_path)) + } + read_model_from_lst(lst_candidates[1]) +} + +#' Derive output directory from model path and read from .lst file +#' @noRd +read_model_from_lst_path <- function(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) + output_dir <- file.path(parent_dir, base_name) + + if (!dir.exists(output_dir)) { + rlang::abort(paste0( + "Output directory not found for model: ", + mod_path, + "\nExpected: ", + output_dir + )) + } + + read_model_from_lst_dir(output_dir) +} + +#' Extract all parameter comments from a model as ModelComments object +#' +#' Parses parameter comments and returns structured metadata for theta, omega, +#' and sigma parameters. +#' +#' For model objects sourced from `.mod`/`.ctl` files: +#' - if run status is `"run"`, metadata is read from the corresponding `.lst` +#' - otherwise (`"not_run"`/`"running"`), metadata is read from the model file +#' +#' @param mod A hyperion_nonmem_model object or path to a run output directory +#' containing an .lst file. +#' @param lookup_path Optional path to a TOML lookup file. If provided, fills +#' NULL fields (display, description, unit, parameterization) from the lookup. +#' +#' @return A `ModelComments` object containing theta, omega, and sigma comments. +#' +#' @section Comment Parsing: +#' Comments are parsed by pharos according to the `[nonmem.comments]` section +#' of `pharos.toml`. Set `type = "type1"` for strict structured comments, or +#' `type = "type2"` for a more flexible structured grammar. See pharos +#' documentation for accepted formats. +#' +#' @seealso [get_parameter_transform()], [get_theta_names()], [get_comment()] +#' @export +get_model_parameter_info <- function(mod, lookup_path = NULL) { + if (is.character(mod) && length(mod) == 1) { + mod_path <- normalizePath(mod, mustWork = FALSE) + if (!dir.exists(mod_path)) { + rlang::abort(paste0( + "mod must be a run output directory containing an .lst file: ", + mod_path + )) + } + mod <- read_model_from_lst_dir(mod_path) + } else if (inherits(mod, "hyperion_nonmem_model")) { + mod_path <- attr(mod, "model_source") %||% "unknown" + if (!identical(mod_path, "unknown")) { + mod_path <- from_config_relative(mod_path) + } + # If model was read from .mod/.ctl file: + # - use .lst for completed runs + # - keep model object for not_run/running + if (!grepl("\\.lst$", mod_path, ignore.case = TRUE)) { + run_status <- refresh_run_status(mod) + if (identical(run_status, "run")) { + if (identical(mod_path, "unknown")) { + rlang::abort( + "Cannot locate .lst for completed run: model_source attribute is missing." + ) + } + # Derive output directory from model path (e.g., run001.mod -> run001/) + mod <- read_model_from_lst_path(mod_path) + } else if (!run_status %in% c("not_run", "running")) { + rlang::abort(paste0( + "model run_status must be 'run', 'running', or 'not_run', got: ", + run_status + )) + } + } + } else { + rlang::abort( + "mod must be a hyperion_nonmem_model object or path to a run output directory containing an .lst file" + ) + } + + mod_path <- attr(mod, "model_source") %||% "unknown" + if (!identical(mod_path, "unknown")) { + mod_path <- from_config_relative(mod_path) + } + + info <- get_model_comment_info(mod) + + coerce_field <- function(field, value) { + if (identical(field, "associated_theta")) { + if (length(value) == 0) NULL else as.character(unlist(value)) + } else { + value + } + } + + build_named_list <- function(entries, constructor, fields) { + if (length(entries) == 0) { + return(list()) + } + keys <- vapply(entries, function(e) e[[1]], character(1)) + out <- lapply(entries, function(e) { + data <- e[[2]] + data_args <- lapply(names(data), function(f) coerce_field(f, data[[f]])) + names(data_args) <- names(data) + args <- c( + list( + constructor = constructor, + fields = fields, + mod_path = mod_path, + nonmem_name = e[[1]] + ), + data_args + ) + do.call(create_comment_with_sources, args) + }) + names(out) <- keys + out + } + + theta_comments <- build_named_list(info$thetas, ThetaComment, theta_fields()) + omega_comments <- build_named_list(info$omegas, OmegaComment, omega_fields()) + sigma_comments <- build_named_list(info$sigmas, SigmaComment, sigma_fields()) + + result <- ModelComments( + theta = theta_comments, + omega = omega_comments, + sigma = sigma_comments + ) + + if (!is.null(lookup_path)) { + lookup_path <- normalizePath(lookup_path, mustWork = FALSE) + result <- apply_lookup(result, lookup_path) + } + + result +} diff --git a/R/pharos-config.R b/R/pharos-config.R index 6a121c3c..e1b8bca8 100644 --- a/R/pharos-config.R +++ b/R/pharos-config.R @@ -1,16 +1,20 @@ -#' Set comment type to type1 in pharos.toml +#' Set comment parsing type in pharos.toml #' -#' Modifies the pharos.toml configuration file to use type1 comment parsing. -#' This is useful when NONMEM control streams use the type1 comment format. +#' Writes `type = ` under the `[nonmem.comments]` section of +#' `pharos.toml`. #' +#' @param type Comment parsing type. One of `"type1"` (strict structured) or +#' `"type2"` (flexible structured grammar). #' @param path Path to pharos.toml. If NULL, finds it automatically. #' @return The path to the modified pharos.toml file (invisibly). #' @export #' #' @examples \dontrun{ -#' use_type1_comments() +#' use_comments("type2") #' } -use_type1_comments <- function(path = NULL) { +use_comments <- function(type = c("type1", "type2"), path = NULL) { + type <- match.arg(type) + if (is.null(path)) { path <- find_pharos_config_file() if (grepl("No pharos.toml", path)) { @@ -20,9 +24,16 @@ use_type1_comments <- function(path = NULL) { toml <- tomledit::read_toml(path) nonmem <- tomledit::get_item(toml, "nonmem") - nonmem$comments$type <- "type1" + nonmem$comments$type <- type toml <- tomledit::insert_items(toml, nonmem = nonmem) tomledit::write_toml(toml, path) invisible(path) } + +#' @rdname use_comments +#' @export +use_type1_comments <- function(path = NULL) { + .Deprecated("use_comments", old = "use_type1_comments") + use_comments(type = "type1", path = path) +} diff --git a/R/summary-methods.R b/R/summary-methods.R index 7c5c2bf3..c29597a5 100644 --- a/R/summary-methods.R +++ b/R/summary-methods.R @@ -334,7 +334,7 @@ print_not_run_summary <- function(x) { #' @param digits Number of significant digits (uses global option if NULL) #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_nonmem_summary_running) +#' @exportS3Method base::print hyperion_nonmem_summary_running print.hyperion_nonmem_summary_running <- function(x, digits = NULL, ...) { print_running_summary(x, digits) invisible(x) @@ -345,7 +345,7 @@ print.hyperion_nonmem_summary_running <- function(x, digits = NULL, ...) { #' @param x A hyperion_nonmem_summary_not_run object #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_nonmem_summary_not_run) +#' @exportS3Method base::print hyperion_nonmem_summary_not_run print.hyperion_nonmem_summary_not_run <- function(x, ...) { print_not_run_summary(x) invisible(x) @@ -357,7 +357,7 @@ print.hyperion_nonmem_summary_not_run <- function(x, ...) { #' @param digits Number of significant digits (uses global option if NULL) #' @param ... Additional arguments (ignored) #' @return Invisible copy of x -#' @rawNamespace S3method(base::print, hyperion_nonmem_summary) +#' @exportS3Method base::print hyperion_nonmem_summary print.hyperion_nonmem_summary <- function(x, digits = NULL, ...) { parts <- build_summary_display_parts(x, digits) diff --git a/R/tree-methods.R b/R/tree-methods.R index 616d6cd9..249d2811 100644 --- a/R/tree-methods.R +++ b/R/tree-methods.R @@ -7,33 +7,52 @@ build_tree_display_parts <- function(x) { )) } - tree_data <- build_cli_tree_data(x) + # Index the Vec by name for O(1) lookup downstream. + nodes_by_name <- x$nodes + names(nodes_by_name) <- vapply(x$nodes, function(n) n$name, character(1)) + + tree_data <- build_cli_tree_data(nodes_by_name) total_models <- length(tree_data$parent) all_parents <- tree_data$parent all_children <- unlist(tree_data$children) root_nodes <- setdiff(all_parents, all_children) + # Models the caller named explicitly (positional, from, to) get + # highlighted in the print. The `focal` attribute is set by the rust + # `get_model_lineage` wrapper. + focal <- attr(x, "focal") %||% character() + focal_display <- gsub("\\.(mod|ctl)$", "", focal) + list( is_empty = FALSE, title = "Hyperion Model Tree", tree_data = tree_data, total_models = total_models, root_nodes = root_nodes, - nodes = x$nodes + nodes = nodes_by_name, + focal_display = focal_display ) } #' Print Method for Hyperion Tree Objects #' #' Displays a hyperion_nonmem_tree in a readable tree format using cli::tree(). -#' Shows the hierarchical relationships between models with Unicode tree characters. +#' Shows the hierarchical relationships between models with Unicode tree +#' characters. The default compact view shows only the model description; pass +#' `verbose = TRUE` for a flat table that also includes tags. #' #' @param x A hyperion_nonmem_tree object +#' @param verbose Logical; if `TRUE`, render a flat table with model, +#' description, and tags columns instead of the tree view. #' @param ... Additional arguments (currently unused) #' #' @return Invisibly returns the input object -#' @rawNamespace S3method(base::print, hyperion_nonmem_tree) -print.hyperion_nonmem_tree <- function(x, ...) { +#' @exportS3Method base::print hyperion_nonmem_tree +print.hyperion_nonmem_tree <- function( + x, + verbose = isTRUE(attr(x, "verbose")), + ... +) { cli::cli_text("") parts <- build_tree_display_parts(x) @@ -47,6 +66,25 @@ print.hyperion_nonmem_tree <- function(x, ...) { cli::cli_alert_info("Models: {parts$total_models}") cli::cli_text("") + if (verbose) { + print_tree_table(parts) + } else { + print_tree_compact(parts) + } + + invisible(x) +} + +# Look up a node's model record by stripped name. +tree_node_model <- function(parts, node_name) { + mod_key <- paste0(node_name, ".mod") + ctl_key <- paste0(node_name, ".ctl") + node_key <- if (mod_key %in% names(parts$nodes)) mod_key else ctl_key + parts$nodes[[node_key]]$model +} + +print_tree_compact <- function(parts) { + width <- cli::console_width() final_output <- character() for (root_idx in seq_along(parts$root_nodes)) { @@ -56,7 +94,6 @@ print.hyperion_nonmem_tree <- function(x, ...) { for (i in seq_along(tree_output)) { line <- tree_output[i] node_name <- gsub("^[^a-zA-Z0-9._]*", "", line) - node_key <- paste0(node_name, ".mod") is_root <- (node_name %in% parts$root_nodes) children <- parts$tree_data$children[ @@ -65,34 +102,35 @@ print.hyperion_nonmem_tree <- function(x, ...) { is_leaf <- length(children) == 0 tree_prefix <- gsub(node_name, "", line, fixed = TRUE) + is_focal <- node_name %in% parts$focal_display + display_name <- if (is_focal) { + cli::style_bold(cli::style_underline(node_name)) + } else { + node_name + } colored_node <- if (is_root) { - cli::col_blue(cli::style_bold(node_name)) + cli::col_blue(cli::style_bold(display_name)) } else if (is_leaf) { - cli::col_green(node_name) + cli::col_green(display_name) } else { - cli::col_yellow(node_name) + cli::col_yellow(display_name) } - if ( - node_key %in% - names(parts$nodes) && - !is.null(parts$nodes[[node_key]]$description) - ) { - desc_text <- parts$nodes[[node_key]]$description - if (nchar(desc_text) > 50) { - desc_text <- paste0(substr(desc_text, 1, 47), "...") + node_model <- tree_node_model(parts, node_name) + has_desc <- !is.null(node_model) && + !is.null(node_model$description) && + nzchar(node_model$description) + suffix <- "" + if (has_desc) { + used <- cli::ansi_nchar(tree_prefix) + nchar(node_name) + 1 + budget <- max(30, width - used - 1) + desc_text <- node_model$description + if (nchar(desc_text) > budget) { + desc_text <- paste0(substr(desc_text, 1, budget - 3), "...") } - final_output <- c( - final_output, - paste0( - tree_prefix, - colored_node, - cli::style_dim(paste0(" - ", desc_text)) - ) - ) - } else { - final_output <- c(final_output, paste0(tree_prefix, colored_node)) + suffix <- paste0(" ", cli::style_dim(desc_text)) } + final_output <- c(final_output, paste0(tree_prefix, colored_node, suffix)) } if (root_idx < length(parts$root_nodes)) { @@ -101,7 +139,138 @@ print.hyperion_nonmem_tree <- function(x, ...) { } cat(final_output, sep = "\n") - invisible(x) +} + +format_hash <- function(h) { + if (is.null(h) || !is.character(h) || !nzchar(h)) { + return("") + } + if (nchar(h) > 8) paste0(substr(h, 1, 8), "...") else h +} + +# Full node record (not just $model) for hash lookups. +tree_node_record <- function(parts, node_name) { + mod_key <- paste0(node_name, ".mod") + ctl_key <- paste0(node_name, ".ctl") + node_key <- if (mod_key %in% names(parts$nodes)) mod_key else ctl_key + parts$nodes[[node_key]] +} + +# Hard upper bounds for content-sized columns; columns shrink to fit content +# when the longest value is shorter. +MAX_DESC_WIDTH <- 60 +MAX_TAGS_WIDTH <- 80 + +print_tree_table <- function(parts) { + # Collect rows in DFS (tree) order so the table reads top-to-bottom like the + # tree view. Parent column preserves lineage info that the flat layout loses. + rows <- list() + for (root_node in parts$root_nodes) { + tree_output <- cli::tree(parts$tree_data, root = root_node) + for (line in tree_output) { + node_name <- gsub("^[^a-zA-Z0-9._]*", "", line) + node <- tree_node_record(parts, node_name) + node_model <- node$model + node_run <- node$run + + parent <- if ( + !is.null(node_model$based_on) && + length(node_model$based_on) > 0 + ) { + gsub("\\.(mod|ctl)$", "", node_model$based_on[[1]]) + } else { + "" + } + desc <- if (!is.null(node_model$description)) { + node_model$description + } else { + "" + } + tags <- if (length(node_model$tags) > 0) { + paste(node_model$tags, collapse = ", ") + } else { + "" + } + model_hash <- format_hash(node_run$start$model_hashes$blake3) + dataset_hash <- format_hash(node_run$start$dataset_hashes$blake3) + + rows[[length(rows) + 1]] <- list( + model = node_name, + parent = parent, + description = desc, + tags = tags, + model_hash = model_hash, + dataset_hash = dataset_hash + ) + } + } + + col_w <- function(col, label) { + max(c(nchar(label), vapply(rows, function(r) nchar(r[[col]]), integer(1)))) + } + model_w <- col_w("model", "Model") + parent_w <- col_w("parent", "Parent") + desc_w <- min(col_w("description", "Description"), MAX_DESC_WIDTH) + tags_w <- min(col_w("tags", "Tags"), MAX_TAGS_WIDTH) + model_hash_w <- col_w("model_hash", "Model Hash") + dataset_hash_w <- col_w("dataset_hash", "Dataset Hash") + + truncate <- function(s, w) { + if (nchar(s) > w) paste0(substr(s, 1, w - 3), "...") else s + } + fmt_row <- function(model, parent, desc, tags, mh, dh) { + sprintf( + "%-*s %-*s %-*s %-*s %-*s %-*s", + model_w, + model, + parent_w, + parent, + desc_w, + truncate(desc, desc_w), + tags_w, + truncate(tags, tags_w), + model_hash_w, + mh, + dataset_hash_w, + dh + ) + } + + # 2 spaces between 6 cols = 10 separator chars. + total_w <- model_w + + parent_w + + desc_w + + tags_w + + model_hash_w + + dataset_hash_w + + 10 + cat( + cli::style_bold(fmt_row( + "Model", + "Parent", + "Description", + "Tags", + "Model Hash", + "Dataset Hash" + )), + "\n", + sep = "" + ) + cat(strrep("\u2500", total_w), "\n", sep = "") + for (r in rows) { + cat( + fmt_row( + r$model, + r$parent, + r$description, + r$tags, + r$model_hash, + r$dataset_hash + ), + "\n", + sep = "" + ) + } } #' Build Tree Data for cli::tree() @@ -113,24 +282,21 @@ print.hyperion_nonmem_tree <- function(x, ...) { #' @return A data frame suitable for cli::tree() #' @keywords internal #' @noRd -build_cli_tree_data <- function(hyperion_nonmem_tree) { - all_nodes <- names(hyperion_nonmem_tree$nodes) +build_cli_tree_data <- function(nodes_by_name) { + all_nodes <- names(nodes_by_name) - # Build children map and find unique nodes in one pass + # Build children map. Only treat a based_on reference as a parent edge if + # the parent is actually in the tree — pharos slices intentionally exclude + # ancestors outside the slice (e.g. `from = run002` excludes run001), so + # synthesizing those parents would put a phantom "root" above the slice. children_map <- list() - unique_nodes <- all_nodes - for (node_name in all_nodes) { - node_info <- hyperion_nonmem_tree$nodes[[node_name]] - if (length(node_info$based_on) > 0) { - parent <- node_info$based_on[[1]] - - # Add any parent to unique nodes if not already present - if (!(parent %in% unique_nodes)) { - unique_nodes <- c(parent, unique_nodes) + node_info <- nodes_by_name[[node_name]] + if (length(node_info$model$based_on) > 0) { + parent <- node_info$model$based_on[[1]] + if (!(parent %in% all_nodes)) { + next } - - # Build children map if (is.null(children_map[[parent]])) { children_map[[parent]] <- character(0) } @@ -138,6 +304,8 @@ build_cli_tree_data <- function(hyperion_nonmem_tree) { } } + unique_nodes <- all_nodes + # Ensure all nodes have entries in children_map for (node in unique_nodes) { if (is.null(children_map[[node]])) { @@ -148,15 +316,17 @@ build_cli_tree_data <- function(hyperion_nonmem_tree) { # Create result data frame data.frame( stringsAsFactors = FALSE, - parent = gsub("\\.mod$", "", unique_nodes), + parent = gsub("\\.(mod|ctl)$", "", unique_nodes), children = I(lapply(unique_nodes, function(node) { - gsub("\\.mod$", "", children_map[[node]]) + gsub("\\.(mod|ctl)$", "", children_map[[node]]) })) ) } #' Knit print method for hyperion_nonmem_tree objects (for Quarto/R Markdown) -#' @param x A hyperion_nonmem_tree object +#' @param x A hyperion_nonmem_tree object. If the object carries a `"verbose"` +#' attribute (set by `get_model_lineage(verbose = TRUE)`), the flat table +#' layout is rendered instead of the tree view. #' @param ... Additional arguments (ignored) #' @return HTML/markdown output for rendered documents #' @exportS3Method knitr::knit_print @@ -187,24 +357,88 @@ knit_print.hyperion_nonmem_tree <- function(x, ...) { "" ) - for (root_idx in seq_along(parts$root_nodes)) { - root_node <- parts$root_nodes[root_idx] - tree_lines <- knit_print_tree_node( - root_node, - parts$tree_data, - parts$nodes, - level = 0 - ) - output <- c(output, tree_lines) + if (isTRUE(attr(x, "verbose"))) { + output <- c(output, knit_print_tree_table(parts)) + } else { + for (root_idx in seq_along(parts$root_nodes)) { + root_node <- parts$root_nodes[root_idx] + tree_lines <- knit_print_tree_node( + root_node, + parts$tree_data, + parts$nodes, + parts$focal_display, + level = 0 + ) + output <- c(output, tree_lines) - if (root_idx < length(parts$root_nodes)) { - output <- c(output, "") + if (root_idx < length(parts$root_nodes)) { + output <- c(output, "") + } } } knitr::asis_output(paste(output, collapse = "\n")) } +# Markdown table renderer for knit_print verbose mode. Mirrors the columns in +# print_tree_table so the two views are consistent. +knit_print_tree_table <- function(parts) { + rows <- list() + for (root_node in parts$root_nodes) { + tree_output <- cli::tree(parts$tree_data, root = root_node) + for (line in tree_output) { + node_name <- gsub("^[^a-zA-Z0-9._]*", "", line) + node <- tree_node_record(parts, node_name) + node_model <- node$model + node_run <- node$run + + parent <- if ( + !is.null(node_model$based_on) && + length(node_model$based_on) > 0 + ) { + gsub("\\.(mod|ctl)$", "", node_model$based_on[[1]]) + } else { + "" + } + desc <- if (!is.null(node_model$description)) { + node_model$description + } else { + "" + } + tags <- if (length(node_model$tags) > 0) { + paste(node_model$tags, collapse = ", ") + } else { + "" + } + model_hash <- format_hash(node_run$start$model_hashes$blake3) + dataset_hash <- format_hash(node_run$start$dataset_hashes$blake3) + + rows[[length(rows) + 1]] <- c( + node_name, + parent, + desc, + tags, + model_hash, + dataset_hash + ) + } + } + + header <- c( + "| Model | Parent | Description | Tags | Model Hash | Dataset Hash |", + "|---|---|---|---|---|---|" + ) + body <- vapply( + rows, + function(r) { + paste0("| ", paste(r, collapse = " | "), " |") + }, + character(1) + ) + + c(header, body) +} + #' Helper function to recursively build tree structure in markdown #' @param node_name Current node name #' @param tree_data Tree data structure from build_cli_tree_data @@ -213,14 +447,22 @@ knit_print.hyperion_nonmem_tree <- function(x, ...) { #' @return Character vector of markdown lines for this subtree #' @keywords internal #' @noRd -knit_print_tree_node <- function(node_name, tree_data, nodes_info, level = 0) { +knit_print_tree_node <- function( + node_name, + tree_data, + nodes_info, + focal_display, + level = 0 +) { output <- character() # Create indentation indent <- paste(rep(" ", level), collapse = "") # Find node info - node_key <- paste0(node_name, ".mod") + mod_key <- paste0(node_name, ".mod") + ctl_key <- paste0(node_name, ".ctl") + node_key <- if (mod_key %in% names(nodes_info)) mod_key else ctl_key # Determine node type for styling all_parents <- tree_data$parent @@ -230,37 +472,51 @@ knit_print_tree_node <- function(node_name, tree_data, nodes_info, level = 0) { is_root <- (node_name %in% root_nodes) children <- tree_data$children[tree_data$parent == node_name][[1]] is_leaf <- length(children) == 0 + is_focal <- node_name %in% focal_display # Apply HTML styling based on node type + display_name <- if (is_focal) { + paste0('', node_name, '') + } else { + node_name + } styled_node <- if (is_root) { - paste0('', node_name, '') + paste0('', display_name, '') } else if (is_leaf) { - paste0('', node_name, '') + paste0('', display_name, '') } else { - paste0('', node_name, '') + paste0('', display_name, '') } - # Add description if available - if ( - node_key %in% - names(nodes_info) && - !is.null(nodes_info[[node_key]]$description) - ) { - desc_text <- nodes_info[[node_key]]$description + # Add tags and description if available + node_model <- nodes_info[[node_key]]$model + has_tags <- !is.null(node_model) && length(node_model$tags) > 0 + has_desc <- !is.null(node_model) && + !is.null(node_model$description) && + nzchar(node_model$description) + suffix <- "" + if (has_tags) { + suffix <- paste0( + ' ', + paste(node_model$tags, collapse = ", "), + '' + ) + } + if (has_desc) { + desc_text <- node_model$description if (nchar(desc_text) > 50) { desc_text <- paste0(substr(desc_text, 1, 47), "...") } - node_line <- paste0( - indent, - "- ", - styled_node, - ' - ', + sep <- if (has_tags) ' | ' else ' ' + suffix <- paste0( + suffix, + sep, + '', desc_text, '' ) - } else { - node_line <- paste0(indent, "- ", styled_node) } + node_line <- paste0(indent, "- ", styled_node, suffix) output <- c(output, node_line) @@ -271,6 +527,7 @@ knit_print_tree_node <- function(node_name, tree_data, nodes_info, level = 0) { child, tree_data, nodes_info, + focal_display, level + 1 ) output <- c(output, child_lines) @@ -284,154 +541,77 @@ knit_print_tree_node <- function(node_name, tree_data, nodes_info, level = 0) { # Lineage utility functions # ============================================================================== -#' Normalize model names with or without .mod suffix -#' -#' @param model_name Character model name -#' @param keep_suffix Logical, if TRUE preserves existing suffix or adds .mod -#' @return Normalized model name -#' @noRd -normalize_model_name <- function(model_name, keep_suffix = FALSE) { - suffix <- NULL - if (grepl("\\.mod$", model_name)) { - suffix <- ".mod" - } else if (grepl("\\.ctl$", model_name)) { - suffix <- ".ctl" - } - clean <- sub("\\.(mod|ctl)$", "", model_name) - if (keep_suffix) { - return(paste0(clean, suffix %||% ".mod")) - } - clean -} - #' Get a model's ancestors #' -#' Walk up the based_on chain to find all ancestors of a model. -#' -#' @param lineage A hyperion_nonmem_tree object from `get_model_lineage()` -#' @param model_name Character, model name (e.g., "run001" or "run001.mod") -#' @return Character vector of ancestor names (without .mod suffix), -#' ordered from parent to root. Returns empty vector if no ancestors. +#' @param mod A `hyperion_nonmem_model` object or a path to a `.mod`/`.ctl` +#' file. +#' @return Character vector of ancestor project-relative paths (with +#' extension, e.g., `"models/onecmt/run001.mod"`). Includes `mod` itself +#' alongside its ancestors. Returns empty vector if the lineage has no +#' ancestors. #' @export -get_model_ancestors <- function(lineage, model_name) { - if (!inherits(lineage, "hyperion_nonmem_tree")) { - rlang::abort("lineage must be a hyperion_nonmem_tree object") - } - - # Normalize model name (add .mod if needed) - model_key <- normalize_model_name(model_name, keep_suffix = TRUE) - - ancestors <- character(0) - current <- model_key - visited <- character(0) - - # Walk up the based_on chain - - while (TRUE) { - if (current %in% visited) { - rlang::abort(sprintf("Circular lineage detected at %s", current)) - } - visited <- c(visited, current) - node <- lineage$nodes[[current]] - if (is.null(node) || length(node$based_on) == 0) { - break - } - parent <- node$based_on[[1]] - # Normalize parent name - parent_clean <- normalize_model_name(parent) - ancestors <- c(ancestors, parent_clean) - current <- normalize_model_name(parent, keep_suffix = TRUE) - } - - ancestors +get_model_ancestors <- function(mod) { + nodes <- get_model_lineage(to = mod)$nodes + vapply(nodes, function(n) n$name, character(1)) } #' Get a model's descendants #' -#' Find all models whose based_on chain includes the given model. -#' -#' @param lineage A hyperion_nonmem_tree object from `get_model_lineage()` -#' @param model_name Character, model name (e.g., "run001" or "run001.mod") -#' @return Character vector of descendant names (without .mod suffix) +#' @param mod A `hyperion_nonmem_model` object or a path to a `.mod`/`.ctl` +#' file. +#' @return Character vector of descendant project-relative paths (with +#' extension, e.g., `"models/onecmt/run002.mod"`). Does not include +#' `mod` itself. #' @export -get_model_descendants <- function(lineage, model_name) { - if (!inherits(lineage, "hyperion_nonmem_tree")) { - rlang::abort("lineage must be a hyperion_nonmem_tree object") - } - - # Normalize model name (remove .mod if present) - model_clean <- normalize_model_name(model_name) - - descendants <- character(0) - - # Build parent -> children map once - parent_map <- list() - for (node_name in names(lineage$nodes)) { - node <- lineage$nodes[[node_name]] - if (!is.null(node) && length(node$based_on) > 0) { - parent_clean <- normalize_model_name(node$based_on[[1]]) - child_clean <- normalize_model_name(node_name) - parent_map[[parent_clean]] <- unique(c( - parent_map[[parent_clean]], - child_clean - )) - } - } - - # Traverse descendants from the starting model - queue <- model_clean - visited <- character(0) - - while (length(queue) > 0) { - current <- queue[[1]] - queue <- queue[-1] - children <- parent_map[[current]] - if (length(children) == 0) { - next - } - for (child in children) { - if (child %in% visited) { - next - } - visited <- c(visited, child) - descendants <- c(descendants, child) - queue <- c(queue, child) - } - } - - descendants +get_model_descendants <- function(mod) { + from_keys <- vapply( + get_model_lineage(from = mod)$nodes, + function(n) n$name, + character(1) + ) + # `slice(from = mod, to = mod)` resolves to just `mod` itself; strip it + # out so the result is descendants only. + self_key <- vapply( + get_model_lineage(from = mod, to = mod)$nodes, + function(n) n$name, + character(1) + ) + setdiff(from_keys, self_key) } #' Check if two models are in a direct lineage #' -#' Returns TRUE if model1 is an ancestor of model2 or vice versa -#' (i.e., they are in a direct parent-child chain). +#' Returns TRUE if `m1` is an ancestor of `m2` or vice versa (i.e., they +#' are in a direct parent-child chain). #' -#' @param lineage A hyperion_nonmem_tree object from `get_model_lineage()` -#' @param model1 Character, model name (e.g., "run001" or "run001.mod") -#' @param model2 Character, model name (e.g., "run003" or "run003.mod") -#' @return Logical, TRUE if models are in direct lineage +#' @param m1 A `hyperion_nonmem_model` object or a path to a `.mod`/`.ctl` +#' file. +#' @param m2 A `hyperion_nonmem_model` object or a path to a `.mod`/`.ctl` +#' file. +#' @return Logical, TRUE if models are in direct lineage. #' @export -are_models_in_lineage <- function(lineage, model1, model2) { - if (!inherits(lineage, "hyperion_nonmem_tree")) { - rlang::abort("lineage must be a hyperion_nonmem_tree object") - } - - # Normalize model names - model1_clean <- normalize_model_name(model1) - model2_clean <- normalize_model_name(model2) - - # Check if model1 is ancestor of model2 - ancestors2 <- get_model_ancestors(lineage, model2) - if (model1_clean %in% ancestors2) { - return(TRUE) - } +are_models_in_lineage <- function(m1, m2) { + length(get_model_lineage(from = m1, to = m2)$nodes) > 0 || + length(get_model_lineage(from = m2, to = m1)$nodes) > 0 +} - # Check if model2 is ancestor of model1 - ancestors1 <- get_model_ancestors(lineage, model1) - if (model2_clean %in% ancestors1) { - return(TRUE) +# Override the extendr-generated `get_model_lineage` to accept `verbose` and +# stash it on the returned tree as an attribute. Both `print()` and +# `knit_print()` read this attribute, so `get_model_lineage(verbose = TRUE)` +# at the REPL or in a knitr chunk renders the flat-table view without the +# caller having to remember the print-time flag. +#' +#' @rdname get_model_lineage +#' @param verbose Logical; if `TRUE`, the returned tree carries a +#' `"verbose"` attribute that causes `print()` and `knit_print()` to render +#' a flat 6-column table (Model, Parent, Description, Tags, Model Hash, +#' Dataset Hash) instead of the tree view. +#' @export +get_model_lineage <- (function() { + raw <- get_model_lineage + function(model = NULL, from = NULL, to = NULL, verbose = FALSE) { + tree <- raw(model = model, from = from, to = to) + attr(tree, "verbose") <- isTRUE(verbose) + tree } - - FALSE -} +})() diff --git a/R/utils.R b/R/utils.R index 5d2a21f7..6d0a7d18 100644 --- a/R/utils.R +++ b/R/utils.R @@ -468,6 +468,28 @@ hyperion_options_message <- function() { "\n" ) + # Show the hyperion.config_dir override status first since it controls + # where pharos.toml gets resolved from. + config_dir_value <- getOption("hyperion.config_dir") + if (!is.null(config_dir_value) && nzchar(config_dir_value)) { + msg <- paste0( + msg, + cli::col_green(cli::symbol$tick), + " ", + "hyperion.config_dir : ", + config_dir_value, + "\n" + ) + } else { + msg <- paste0( + msg, + cli::symbol$info, + " ", + cli::style_dim("hyperion.config_dir : (unset)"), + "\n" + ) + } + if (grepl("No pharos.toml config file found", pharos_config_status)) { msg <- paste0( msg, diff --git a/R/zzz.R b/R/zzz.R index 591216cc..33102105 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,6 +1,6 @@ .onLoad <- function(libname, pkgname) { S7::methods_register() - set_panic_message() + silence_panic_output() # Set default hyperion options if not already set if (is.null(getOption("hyperion.significant_number_display"))) { diff --git a/_starlightr.toml b/_starlightr.toml index 4f00cf4f..f93d0959 100644 --- a/_starlightr.toml +++ b/_starlightr.toml @@ -13,7 +13,8 @@ include_pagefind = true [versions] enabled = true list = [ - { tag = "0.4.1", label = "v0.4.1", default = true }, + { tag = "0.4.2", label = "v0.4.2", default = true }, + { tag = "0.4.1", label = "v0.4.1" }, { tag = "0.3.2", label = "v0.3.2" }, { tag = "0.3.1", label = "v0.3.1" }, { tag = "0.3.0", label = "v0.3.0" }, diff --git a/inst/extdata/mod/1001.mod b/inst/extdata/mod/1001.mod index fe1d6232..d7940039 100644 --- a/inst/extdata/mod/1001.mod +++ b/inst/extdata/mod/1001.mod @@ -52,8 +52,8 @@ $THETA 1 FIX ;F1 (fraction) $OMEGA - 0.1 ;OM1 CL :EXP - 0.1 ;OM2 VC :EXP + 0.1 ;OM1 CL/F :EXP + 0.1 ;OM2 VC/F :EXP 0.1 ;OM3 KA :EXP diff --git a/inst/extdata/mod/everything.mod b/inst/extdata/mod/everything.mod index a747a221..96539e36 100644 --- a/inst/extdata/mod/everything.mod +++ b/inst/extdata/mod/everything.mod @@ -1,22 +1,39 @@ $PROBLEM Some header #2 $INPUT ID TIME DV DOSE=AMT DV WT AGE SEX CREA DATE=DROP -$DATA ..\data.csv IGNORE=# +$DATA "..\path with spaces\data.csv" IGNORE=# IGNORE=(DVID.EQ.3) - IGNORE=(ID.EQ.3.14) - ACCEPT=(AGE.GT.3,SEX.EQ.1) + IGN(ID.EQ.3.14) + IGNORE=(DVID==3) + IGNORE=(AGE>=18) + IGNORE=(AGE>3,AGE<100) + IGNORE=(AGE<=65) + IGNORE=(TYPE/=0) + IGNORE=(TYPE=1) + IGNORE=(TYPE.EQN.1) + IGNORE=(TYPE.NEN.2) + IGNORE=(TYPE 1) RECORDS=200 + NULL=. LAST20=00 -$SUBROUTINES ADVAN4 TRANS4 OTHER=fa.90 +$SUBROUTINES ADVAN4 TOL=9 TRANS4 OTHER=fa.90 +$ABBR REPLACE ETA(1)=ETA(3) +$ABBR REPLACE THETA(1)=THETA(5) +$ABBREVIATED COMRES=5 DERIV2=NO $PK TVCL = THETA(1)*(WT/70)**THETA(6) CL = TVCL * EXP(ETA(1)) $THETA 1.5 (0,0.5,2) ; THETA(1) and THETA(2) +$THETA (-INF, 0.5, 10) ; THETA with -INF lower bound +$THETA (0, 5, INF) ; THETA with INF upper bound +$THETA (0, 0.1)x3 ; Three identical THETAs +$THETA CL=(0, 1.5, 10) ; Named THETA +$THETA NAMES(KA, V2, Q) (0, 0.5) (0, 10) (0, 2) ; NAMES syntax +$THETA NAMES(A, B, C) (1, 1.1)x3 ; Three identical THETAs with NAMES $THETA 2.3 FIX ; THETA(3) $THETA 0.8 0.25 ; THETA(4) and THETA(5) $THETA (1,2.3 FIX) ; THETA(6) (0.75 FIX) ; THETA(7) - $OMEGA 0.04 ; ETA(1) - CL (diagonal) $OMEGA .17 @@ -26,33 +43,56 @@ $OMEGA BLOCK(2) CORR $OMEGA BLOCK(2) SAME ; ETA(4), ETA(5) - same structure as above -$OMEGA -(0,0.1,1 FIX) ; ETA(6) - fixed diagonal +$OMEGA BLOCK(2) FIX ; ETA(7), ETA(8) - same structure as above +0.011207 +0 0.338724 + +$OMEGA BLOCK(4) +0.1 +0.01 0.1 +(0.01)x2 0.1 +(0.01)x3 0.1 $SIGMA BLOCK(2) 0.01 ; Proportional error variance 0.002 0.25 ; Prop-Add covariance, Additive error variance +$SIGMA +1 FIXED +0.0360 + +$OMEGA ECL=.4 ; Label=Value syntax for diagonal +$OMEGA BLOCK(2) +EV1= 0.3 +EQ= 0.01 0.35 ; Label=Value syntax in block + +$OMEGA BLOCK(4) NAMES(ECL2,EV2,EQ2,EV3) VALUES(0.03,0.01) ; NAMES with VALUES + +$OMEGA BLOCK(3) CORR ; flag before values +0.2 +0.3 0.15 +0.1 0.05 0.3 + +$OMEGA BLOCK(3) ; flag after values +0.2 +0.3 0.15 +0.1 0.05 0.3 CORR + +$OMEGA BLOCK(3) ; FIX interleaved among values +6. +.005 FIX .3 +.001 .002 .1 + +$SIGMA PROP=0.04 ; Label=Value syntax for SIGMA +$SIGMA 0.01 0.02 ; diagonal SIGMA + $EST METHOD=0 SLOW $EST MAXEVAL=9999 METHOD=1 INTER PRINT=5 MSFO=../2.MSF $EST MAXEVAL=9999 METHOD=1 INTER PRINT=5 FILE=run001.est $ESTIMATION MAXEVAL=9999 METHOD=IMP INTER FILE=est -$TABLE ID TIME AMT EVID IPRED AGE WT MDV ONEHEADER NOPRINT FILE=../2.TAB +$TABLE ID TIME AMT EVID IPRED AGE WT MDV ETAS(1:LAST) ONEHEADER NOPRINT FILE=../2.TAB $TABLE ID FILE=001.tab $TABLE ID TIME AMT EVID AGE WT MDV KA CL V2 V3 Q BETA HLBE ONEHEADER NOPRINT FILE=../2par.TAB -$SIM ONLYSIM - - - -Parameter Initial Lower Upper Fixed Parametrization Comment ------------ -------- ------ ------ ------ ---------------- -------------------------------------------- -OMEGA(1,1) 0.04 NA NA no ETA(1) - CL (diagonal) -OMEGA(2,2) 0.17 NA NA no -OMEGA(3,3) 0.20 NA NA no Correlation ETA(2) - V (SD) -OMEGA(4,3) 0.30 NA NA no Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) -OMEGA(4,4) 0.15 NA NA no Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) -OMEGA(5,5) 0.20 NA NA no Correlation ETA(4), ETA(5) - same structure as above -OMEGA(6,5) 0.30 NA NA no Correlation ETA(4), ETA(5) - same structure as above -OMEGA(6,6) 0.15 NA NA no Correlation ETA(4), ETA(5) - same structure as above -OMEGA(7,7) 0.10 0 1 yes ETA(6) - fixed diagonal +$MSFI msfb.msf +$SIM (1) (2 NONPARAMETRIC) SUBPROBLEMS=1 diff --git a/inst/extdata/models/1002.mod b/inst/extdata/models/1002.mod new file mode 100644 index 00000000..e69de29b diff --git a/inst/extdata/models/onecmt/run001.mod b/inst/extdata/models/onecmt/run001.mod index 6108172d..f8bce08f 100644 --- a/inst/extdata/models/onecmt/run001.mod +++ b/inst/extdata/models/onecmt/run001.mod @@ -26,18 +26,18 @@ IPRED = F Y = IPRED * (1 + EPS(1)) + EPS(2) $THETA -(0, 1) ; 1. TVCL (L/hr) -(0, 30) ; 2. TVV (L) -(0, 1) ; 3. TVKA (1/hr) +(0, 1) ;TVCL (L/hr) +(0, 30) ;TVV (L) +(0, 1) ;TVKA (1/hr) $OMEGA -0.1 ; 1. OM1 TVCL :EXP -0.1 ; 2. OM2 TVV :EXP -0.1 FIX ; 3. OM3 TVKA :EXP +0.1 ;OM1 TVCL :EXP +0.1 ;OM2 TVV :EXP +0.1 FIX ;OM3 TVKA :EXP $SIGMA -0.04 ; 1. Proportional error (variance, 20% CV) -0.01 FIX ; 2. Additive error (variance, 0.01 mg/L SD) +0.04 ;Proportional error (variance, 20% CV) +0.01 FIX ;Additive error (variance, 0.01 mg/L SD) $ESTIMATION METHOD=1 INTERACTION MAXEVAL=9999 PRINT=5 $COV PRINT=E MATRIX = R diff --git a/inst/extdata/models/onecmt/run001/pharos_start.json b/inst/extdata/models/onecmt/run001/pharos_start.json index 68be040b..13f748a8 100644 --- a/inst/extdata/models/onecmt/run001/pharos_start.json +++ b/inst/extdata/models/onecmt/run001/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2026-01-16T17:08:30+00:00", "model_name": "run001", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/inst/extdata/test_data/models/onecmt/run001.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/onecmt/run001.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/inst/extdata/test_data/data/derived/onecmpt-oral-30ind.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", "dataset_hashes": { "blake3": "8d8189cfc45dc4d56c295ca990a131e086f53d874aa91e730c1e8856e840b005" }, diff --git a/inst/extdata/models/onecmt/run001/run001.lst b/inst/extdata/models/onecmt/run001/run001.lst index 8fcd1cbf..024bc277 100644 --- a/inst/extdata/models/onecmt/run001/run001.lst +++ b/inst/extdata/models/onecmt/run001/run001.lst @@ -27,18 +27,18 @@ IPRED = F Y = IPRED * (1 + EPS(1)) + EPS(2) $THETA -(0, 1) ; 1. TVCL (L/hr) -(0, 30) ; 2. TVV (L) -(0, 1) ; 3. TVKA (1/hr) +(0, 1) ;TVCL (L/hr) +(0, 30) ;TVV (L) +(0, 1) ;TVKA (1/hr) $OMEGA -0.1 ; 1. OM1 TVCL :EXP -0.1 ; 2. OM2 TVV :EXP -0.1 FIX ; 3. OM3 TVKA :EXP +0.1 ;OM1 TVCL :EXP +0.1 ;OM2 TVV :EXP +0.1 FIX ;OM3 TVKA :EXP $SIGMA -0.04 ; 1. Proportional error (variance, 20% CV) -0.01 FIX ; 2. Additive error (variance, 0.01 mg/L SD) +0.04 ;Proportional error (variance, 20% CV) +0.01 FIX ;Additive error (variance, 0.01 mg/L SD) $ESTIMATION METHOD=1 INTERACTION MAXEVAL=9999 PRINT=5 $COV PRINT=E MATRIX = R diff --git a/inst/extdata/models/onecmt/run002/pharos_start.json b/inst/extdata/models/onecmt/run002/pharos_start.json index 995bf078..3117ee1e 100644 --- a/inst/extdata/models/onecmt/run002/pharos_start.json +++ b/inst/extdata/models/onecmt/run002/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2025-11-05T16:04:31+00:00", "model_name": "run002", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/models/onecmt/run002.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/onecmt/run002.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/data/derived/onecmpt-oral-30ind.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", "dataset_hashes": { "blake3": "8d8189cfc45dc4d56c295ca990a131e086f53d874aa91e730c1e8856e840b005" }, diff --git a/inst/extdata/models/onecmt/run002_metadata.json b/inst/extdata/models/onecmt/run002_metadata.json index 585823a9..96df28ae 100644 --- a/inst/extdata/models/onecmt/run002_metadata.json +++ b/inst/extdata/models/onecmt/run002_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run001.mod" + "extdata/models/onecmt/run001.mod" ], "description": "Adding COV step, unfixing eps(2)", "tags": [] diff --git a/inst/extdata/models/onecmt/run002a_metadata.json b/inst/extdata/models/onecmt/run002a_metadata.json index 6ee1b664..62be3a11 100644 --- a/inst/extdata/models/onecmt/run002a_metadata.json +++ b/inst/extdata/models/onecmt/run002a_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run002.mod" + "extdata/models/onecmt/run002.mod" ], "description": "Some description about what makes run002a different", "tags": [] diff --git a/inst/extdata/models/onecmt/run002b001_metadata.json b/inst/extdata/models/onecmt/run002b001_metadata.json index 9c34c697..6f697b18 100644 --- a/inst/extdata/models/onecmt/run002b001_metadata.json +++ b/inst/extdata/models/onecmt/run002b001_metadata.json @@ -1,7 +1,11 @@ { "based_on": [ - "run002.mod" + "extdata/models/onecmt/run002.mod" ], + "copied_from": "", "description": "Jittering initial sigma estimates, using theta/omega final estimates. Adding covariate", - "tags": [] + "tags": [ + "not run", + "2cmt" + ] } diff --git a/inst/extdata/models/onecmt/run003.mod b/inst/extdata/models/onecmt/run003.mod index c081c377..e181b798 100644 --- a/inst/extdata/models/onecmt/run003.mod +++ b/inst/extdata/models/onecmt/run003.mod @@ -32,7 +32,7 @@ $THETA $OMEGA BLOCK(2) 0.1 ;OM1 TVCL :EXP -0.0001 ;OM1,2 TVCL:TVV :EXP +0.0001 ;OM1,2 TVCL,TVV :EXP 0.1 ;OM2 TVV :EXP $OMEGA 0.1 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003/pharos_start.json b/inst/extdata/models/onecmt/run003/pharos_start.json index 4b29df55..2b2e4270 100644 --- a/inst/extdata/models/onecmt/run003/pharos_start.json +++ b/inst/extdata/models/onecmt/run003/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2026-01-08T15:08:59+00:00", "model_name": "run003", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/models/onecmt/run003.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/onecmt/run003.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/data/derived/onecmpt-oral-30ind.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", "dataset_hashes": { "blake3": "8d8189cfc45dc4d56c295ca990a131e086f53d874aa91e730c1e8856e840b005" }, diff --git a/inst/extdata/models/onecmt/run003/run003.lst b/inst/extdata/models/onecmt/run003/run003.lst index 9a7db0f7..39cfe3ae 100644 --- a/inst/extdata/models/onecmt/run003/run003.lst +++ b/inst/extdata/models/onecmt/run003/run003.lst @@ -33,7 +33,7 @@ $THETA $OMEGA BLOCK(2) 0.1 ;OM1 TVCL :EXP -0.0001 ;OM1,2 TVCL:TVV :EXP +0.0001 ;OM1,2 TVCL,TVV :EXP 0.1 ;OM2 TVV :EXP $OMEGA 0.1 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003/run003.mod b/inst/extdata/models/onecmt/run003/run003.mod index c7cdffbf..4d4f693b 100644 --- a/inst/extdata/models/onecmt/run003/run003.mod +++ b/inst/extdata/models/onecmt/run003/run003.mod @@ -32,7 +32,7 @@ $THETA $OMEGA BLOCK(2) 0.1 ;OM1 TVCL :EXP -0.0001 ;OM1,2 TVCL:TVV :EXP +0.0001 ;OM1,2 TVCL,TVV :EXP 0.1 ;OM2 TVV :EXP $OMEGA 0.1 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003_metadata.json b/inst/extdata/models/onecmt/run003_metadata.json index 1da8d7aa..98847f17 100644 --- a/inst/extdata/models/onecmt/run003_metadata.json +++ b/inst/extdata/models/onecmt/run003_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run002.mod" + "extdata/models/onecmt/run002.mod" ], "description": "Jittering initial estimates", "tags": [ diff --git a/inst/extdata/models/onecmt/run003b1.mod b/inst/extdata/models/onecmt/run003b1.mod index b3567bc8..4d692a4e 100644 --- a/inst/extdata/models/onecmt/run003b1.mod +++ b/inst/extdata/models/onecmt/run003b1.mod @@ -34,7 +34,7 @@ $THETA $OMEGA BLOCK(2) 0.103 ;OM1 TVCL :EXP -0.00009 ;OM1,2 TVCL:TVV :EXP +0.00009 ;OM1,2 TVCL,TVV :EXP 0.109 ;OM2 TVV :EXP $OMEGA 0.099 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003b1/pharos_start.json b/inst/extdata/models/onecmt/run003b1/pharos_start.json index 054a4e5c..0e4af0f1 100644 --- a/inst/extdata/models/onecmt/run003b1/pharos_start.json +++ b/inst/extdata/models/onecmt/run003b1/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2026-01-13T19:53:01+00:00", "model_name": "run003b1", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/models/onecmt/run003b1.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/onecmt/run003b1.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind-cov.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/data/derived/onecmpt-oral-30ind-cov.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind-cov.csv", "dataset_hashes": { "blake3": "df6cbcde5998d94795503b6b2dd98cfd01f10b77a7d2e527e02b00b2591f93a5" }, diff --git a/inst/extdata/models/onecmt/run003b1/run003b1.lst b/inst/extdata/models/onecmt/run003b1/run003b1.lst index 2331bc8d..d3e86088 100644 --- a/inst/extdata/models/onecmt/run003b1/run003b1.lst +++ b/inst/extdata/models/onecmt/run003b1/run003b1.lst @@ -35,7 +35,7 @@ $THETA $OMEGA BLOCK(2) 0.103 ;OM1 TVCL :EXP -0.00009 ;OM1,2 TVCL:TVV :EXP +0.00009 ;OM1,2 TVCL,TVV :EXP 0.109 ;OM2 TVV :EXP $OMEGA 0.099 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003b1/run003b1.mod b/inst/extdata/models/onecmt/run003b1/run003b1.mod index a5d28142..ab99a33c 100644 --- a/inst/extdata/models/onecmt/run003b1/run003b1.mod +++ b/inst/extdata/models/onecmt/run003b1/run003b1.mod @@ -34,7 +34,7 @@ $THETA $OMEGA BLOCK(2) 0.103 ;OM1 TVCL :EXP -0.00009 ;OM1,2 TVCL:TVV :EXP +0.00009 ;OM1,2 TVCL,TVV :EXP 0.109 ;OM2 TVV :EXP $OMEGA 0.099 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003b1_metadata.json b/inst/extdata/models/onecmt/run003b1_metadata.json index e98afd84..a62ac31b 100644 --- a/inst/extdata/models/onecmt/run003b1_metadata.json +++ b/inst/extdata/models/onecmt/run003b1_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run003.mod" + "extdata/models/onecmt/run003.mod" ], "description": "Updating run003 to 003b1 with jittered params. Adding WT on V", "tags": [] diff --git a/inst/extdata/models/onecmt/run003b2.mod b/inst/extdata/models/onecmt/run003b2.mod index fb6bbafa..8282c25f 100644 --- a/inst/extdata/models/onecmt/run003b2.mod +++ b/inst/extdata/models/onecmt/run003b2.mod @@ -32,7 +32,7 @@ $THETA $OMEGA BLOCK(2) 0.122 ;OM1 TVCL :EXP -0.074543 ;OM1,2 TVCL:TVV :EXP +0.074543 ;OM1,2 TVCL,TVV :EXP 0.124 ;OM2 TVV :EXP $OMEGA 0.122 ;OM3 TVKA :EXP diff --git a/inst/extdata/models/onecmt/run003b2_metadata.json b/inst/extdata/models/onecmt/run003b2_metadata.json index 7d99ba37..79ba9a7d 100644 --- a/inst/extdata/models/onecmt/run003b2_metadata.json +++ b/inst/extdata/models/onecmt/run003b2_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run003.mod" + "extdata/models/onecmt/run003.mod" ], "description": "Updating run003 with mod object", "tags": [] diff --git a/inst/extdata/models/onecmt/run004.mod b/inst/extdata/models/onecmt/run004.mod index 6ea48660..a14b6777 100644 --- a/inst/extdata/models/onecmt/run004.mod +++ b/inst/extdata/models/onecmt/run004.mod @@ -30,14 +30,16 @@ $THETA (0, 41.98) ;TVV (L) (0, 1.24) ;TVKA (1/hr) +$OMEGA BLOCK(2) +0.1 ;OM1 TVCL :EXP +0.0001 ;OM1,2 TVCL,TVV :EXP +0.1 ;OM2 TVV :EXP $OMEGA -0.126 ;OM1 TVCL -0.133 ;OM2 TVV -0.1 FIX ;OM3 TVKA +0.1 ;OM3 TVKA :EXP $SIGMA -0.0364 ; 1. Proportional error (variance, 20% CV) -0.01 FIX ; 2. Additive error (variance, 0.01 mg/L SD) +0.035738 ;SIG1 Proportional error (variance, 20% CV) +0.006 ;SIG2 Additive error (variance, 0.01 mg/L SD) $ESTIMATION METHOD=1 INTERACTION MAXEVAL=9999 PRINT=5 $TABLE ID TIME DV PRED IPRED CWRES NPDE NOAPPEND NOPRINT ONEHEADER FILE=run004.tab diff --git a/inst/extdata/models/onecmt/run004/pharos_start.json b/inst/extdata/models/onecmt/run004/pharos_start.json index 4b29df55..2b2e4270 100644 --- a/inst/extdata/models/onecmt/run004/pharos_start.json +++ b/inst/extdata/models/onecmt/run004/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2026-01-08T15:08:59+00:00", "model_name": "run003", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/models/onecmt/run003.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/onecmt/run003.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/vignettes/test_data/data/derived/onecmpt-oral-30ind.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", "dataset_hashes": { "blake3": "8d8189cfc45dc4d56c295ca990a131e086f53d874aa91e730c1e8856e840b005" }, diff --git a/inst/extdata/models/onecmt/run004/run004.lst b/inst/extdata/models/onecmt/run004/run004.lst index 9a7db0f7..48dd26cf 100644 --- a/inst/extdata/models/onecmt/run004/run004.lst +++ b/inst/extdata/models/onecmt/run004/run004.lst @@ -712,7 +712,4 @@ Days until program expires : 36 2.79E-01 4.48E-01 5.33E-01 7.74E-01 1.07E+00 1.23E+00 1.26E+00 1.69E+00 1.72E+00 - Elapsed finaloutput time in seconds: 0.01 - #CPUT: Total CPU Time in Seconds, 0.805 -Stop Time: -Thu Jan 8 15:09:13 UTC 2026 + diff --git a/inst/extdata/models/onecmt/run004_metadata.json b/inst/extdata/models/onecmt/run004_metadata.json index 344c6440..5c59f5af 100644 --- a/inst/extdata/models/onecmt/run004_metadata.json +++ b/inst/extdata/models/onecmt/run004_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run001.mod" + "extdata/models/onecmt/run001.mod" ], "description": "Updating run001 to run004 with jittered params and updated initials", "tags": [] diff --git a/inst/extdata/models/onecmt/run005_metadata.json b/inst/extdata/models/onecmt/run005_metadata.json index 344c6440..5c59f5af 100644 --- a/inst/extdata/models/onecmt/run005_metadata.json +++ b/inst/extdata/models/onecmt/run005_metadata.json @@ -1,6 +1,6 @@ { "based_on": [ - "run001.mod" + "extdata/models/onecmt/run001.mod" ], "description": "Updating run001 to run004 with jittered params and updated initials", "tags": [] diff --git a/inst/extdata/models/run-error-names/run-err/pharos_start.json b/inst/extdata/models/run-error-names/run-err/pharos_start.json index 569c841a..bd163369 100644 --- a/inst/extdata/models/run-error-names/run-err/pharos_start.json +++ b/inst/extdata/models/run-error-names/run-err/pharos_start.json @@ -1,9 +1,9 @@ { "start": "2026-02-23T17:49:03+00:00", "model_name": "run-err", - "model_canonical_path": "/data/user-homes/matthews/Packages/hyperion/inst/extdata/models/run-error-names/run-err.mod", + "model_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/models/run-error-names/run-err.mod", "dataset_path": "../../data/derived/onecmpt-oral-30ind.csv", - "dataset_canonical_path": "/data/user-homes/matthews/Packages/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", + "dataset_canonical_path": "/Users/mattsmith/Documents/hyperion/inst/extdata/data/derived/onecmpt-oral-30ind.csv", "dataset_hashes": { "blake3": "8d8189cfc45dc4d56c295ca990a131e086f53d874aa91e730c1e8856e840b005" }, diff --git a/inst/pharos.toml b/inst/pharos.toml index f41f9d85..4bf966f6 100644 --- a/inst/pharos.toml +++ b/inst/pharos.toml @@ -22,7 +22,7 @@ num_cpus = 4 timeout = 2147483647 [nonmem.comments] -type = "type1" +type = "type2" error_on_invalid = false [nonmem.summary] diff --git a/man/OmegaComment.Rd b/man/OmegaComment.Rd index 8d7ca585..d9231543 100644 --- a/man/OmegaComment.Rd +++ b/man/OmegaComment.Rd @@ -7,6 +7,7 @@ OmegaComment( nonmem_name = character(0), name = NULL, + raw_name = NULL, display = NULL, description = NULL, parameterization = NULL, @@ -18,6 +19,9 @@ OmegaComment( \item{name}{Character or NULL. User-defined parameter name (e.g., "IIV-CL").} +\item{raw_name}{Character or NULL. Raw user label before pharos composition; +e.g. \code{"IIV"} when \code{name} is \code{"IIV (CL)"}.} + \item{display}{Character or NULL. Display name for tables/output.} \item{description}{Character or NULL. Description of the parameter.} @@ -34,6 +38,8 @@ Represents parsed comments for OMEGA parameters. \describe{ \item{nonmem_name}{The NONMEM parameter identifier.} \item{name}{User-friendly name parsed from comments.} +\item{raw_name}{Raw user label before pharos composition; e.g. \code{"IIV"} +when \code{name} is \code{"IIV (CL)"}.} \item{display}{Display name for tables. Falls back to \code{name} if NULL.} \item{description}{Longer description of what the parameter represents.} \item{parameterization}{Transformation type. Valid values: diff --git a/man/are_models_in_lineage.Rd b/man/are_models_in_lineage.Rd index ada30c9e..202b5ede 100644 --- a/man/are_models_in_lineage.Rd +++ b/man/are_models_in_lineage.Rd @@ -4,19 +4,19 @@ \alias{are_models_in_lineage} \title{Check if two models are in a direct lineage} \usage{ -are_models_in_lineage(lineage, model1, model2) +are_models_in_lineage(m1, m2) } \arguments{ -\item{lineage}{A hyperion_nonmem_tree object from \code{get_model_lineage()}} +\item{m1}{A \code{hyperion_nonmem_model} object or a path to a \code{.mod}/\code{.ctl} +file.} -\item{model1}{Character, model name (e.g., "run001" or "run001.mod")} - -\item{model2}{Character, model name (e.g., "run003" or "run003.mod")} +\item{m2}{A \code{hyperion_nonmem_model} object or a path to a \code{.mod}/\code{.ctl} +file.} } \value{ -Logical, TRUE if models are in direct lineage +Logical, TRUE if models are in direct lineage. } \description{ -Returns TRUE if model1 is an ancestor of model2 or vice versa -(i.e., they are in a direct parent-child chain). +Returns TRUE if \code{m1} is an ancestor of \code{m2} or vice versa (i.e., they +are in a direct parent-child chain). } diff --git a/man/check_dataset.Rd b/man/check_dataset.Rd index a48b502d..01c46b18 100644 --- a/man/check_dataset.Rd +++ b/man/check_dataset.Rd @@ -15,9 +15,3 @@ Dataset check results \description{ Checks model dataset } -\examples{ -\dontrun{ -model <- read_model("model/nonmem/run001.mod") -model |> check_dataset() -} -} diff --git a/man/clear_metadata_file.Rd b/man/clear_metadata_file.Rd new file mode 100644 index 00000000..30d476e9 --- /dev/null +++ b/man/clear_metadata_file.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{clear_metadata_file} +\alias{clear_metadata_file} +\title{Clear fields in a model's metadata file} +\usage{ +clear_metadata_file( + model_path, + based_on = FALSE, + copied_from = FALSE, + tags = FALSE +) +} +\arguments{ +\item{model_path}{Path to the NONMEM model file, or a hyperion_nonmem_model object} + +\item{based_on}{If TRUE, clear the based_on field. Default FALSE.} + +\item{copied_from}{If TRUE, clear the copied_from field. Default FALSE.} + +\item{tags}{If TRUE, clear the tags field. Default FALSE.} +} +\value{ +Returns invisibly after updating the metadata file +} +\description{ +Selectively clears the \code{based_on}, \code{copied_from}, and/or \code{tags} fields in +the metadata file associated with a model. Fields not selected are left +unchanged. +} +\examples{ +\dontrun{ +clear_metadata_file("model/nonmem/run001.mod", tags = TRUE) +model <- read_model("model/nonmem/run001.mod") +clear_metadata_file(model, based_on = TRUE, copied_from = TRUE) +} +} diff --git a/man/copy_model.Rd b/man/copy_model.Rd index 3d6bcf5e..7a20edbb 100644 --- a/man/copy_model.Rd +++ b/man/copy_model.Rd @@ -13,7 +13,9 @@ copy_model( jitter = NULL, jitter_excluded = NULL, seed = NULL, - description = NULL, + description, + based_on = NULL, + tags = NULL, no_metadata = FALSE ) } @@ -40,6 +42,10 @@ Examples: "THETA1" or c("THETA1")} \item{description}{Description of model in metadata file} +\item{based_on}{Character vector of model names/paths that this model is based on} + +\item{tags}{Character vector of tags to attach to the model in metadata} + \item{no_metadata}{boolean, if true, does not create metadatafile, default FALSE} } \value{ diff --git a/man/format_omega_display_name.Rd b/man/format_omega_display_name.Rd deleted file mode 100644 index 0dba344a..00000000 --- a/man/format_omega_display_name.Rd +++ /dev/null @@ -1,44 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/comments-utils.R -\name{format_omega_display_name} -\alias{format_omega_display_name} -\title{Format omega display name, avoiding duplicate theta info} -\usage{ -format_omega_display_name(name, associated_theta, theta_labels = NULL) -} -\arguments{ -\item{name}{The omega parameter name (e.g., "IIV-CL" or "IIV")} - -\item{associated_theta}{Character vector of associated theta names} - -\item{theta_labels}{Optional named vector mapping theta names to display -labels. If provided, uses labels for the suffix; otherwise uses theta names.} -} -\value{ -The formatted display name with theta info appended only if missing -} -\description{ -Builds a display name for omega parameters by appending associated theta -information, but only if that information isn't already present in the name. -This prevents duplication like "IIV-CL (CL)" when the omega was already -renamed to include the theta. -} -\examples{ -# Theta already in name - no duplication -format_omega_display_name("IIV-CL", "CL") -# Returns: "IIV-CL" - -# Theta not in name - appends it -format_omega_display_name("IIV", "CL") -# Returns: "IIV CL" - -# Multiple thetas -format_omega_display_name("IIV", c("CL", "V")) -# Returns: "IIV CL, V" - -# With custom labels -format_omega_display_name("IIV", "CL", c(CL = "Clearance")) -# Returns: "IIV Clearance" - -} -\keyword{internal} diff --git a/man/get_model_ancestors.Rd b/man/get_model_ancestors.Rd index ba514ad8..bfd37051 100644 --- a/man/get_model_ancestors.Rd +++ b/man/get_model_ancestors.Rd @@ -4,17 +4,18 @@ \alias{get_model_ancestors} \title{Get a model's ancestors} \usage{ -get_model_ancestors(lineage, model_name) +get_model_ancestors(mod) } \arguments{ -\item{lineage}{A hyperion_nonmem_tree object from \code{get_model_lineage()}} - -\item{model_name}{Character, model name (e.g., "run001" or "run001.mod")} +\item{mod}{A \code{hyperion_nonmem_model} object or a path to a \code{.mod}/\code{.ctl} +file.} } \value{ -Character vector of ancestor names (without .mod suffix), -ordered from parent to root. Returns empty vector if no ancestors. +Character vector of ancestor project-relative paths (with +extension, e.g., \code{"models/onecmt/run001.mod"}). Includes \code{mod} itself +alongside its ancestors. Returns empty vector if the lineage has no +ancestors. } \description{ -Walk up the based_on chain to find all ancestors of a model. +Get a model's ancestors } diff --git a/man/get_model_comment_info.Rd b/man/get_model_comment_info.Rd new file mode 100644 index 00000000..3de6ccb5 --- /dev/null +++ b/man/get_model_comment_info.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{get_model_comment_info} +\alias{get_model_comment_info} +\title{Build per-parameter comment info from a model object (internal)} +\usage{ +get_model_comment_info(model) +} +\arguments{ +\item{model}{hyperion_nonmem_model object from read_model()} +} +\value{ +list with \code{thetas}, \code{omegas}, \code{sigmas} entries; each is a list of +length-2 lists \verb{(coordinate, info)} in numeric coordinate order. +} +\description{ +Build per-parameter comment info from a model object (internal) +} +\keyword{internal} diff --git a/man/get_model_descendants.Rd b/man/get_model_descendants.Rd index 2b8efb09..1e0f7fda 100644 --- a/man/get_model_descendants.Rd +++ b/man/get_model_descendants.Rd @@ -4,16 +4,17 @@ \alias{get_model_descendants} \title{Get a model's descendants} \usage{ -get_model_descendants(lineage, model_name) +get_model_descendants(mod) } \arguments{ -\item{lineage}{A hyperion_nonmem_tree object from \code{get_model_lineage()}} - -\item{model_name}{Character, model name (e.g., "run001" or "run001.mod")} +\item{mod}{A \code{hyperion_nonmem_model} object or a path to a \code{.mod}/\code{.ctl} +file.} } \value{ -Character vector of descendant names (without .mod suffix) +Character vector of descendant project-relative paths (with +extension, e.g., \code{"models/onecmt/run002.mod"}). Does not include +\code{mod} itself. } \description{ -Find all models whose based_on chain includes the given model. +Get a model's descendants } diff --git a/man/get_model_lineage.Rd b/man/get_model_lineage.Rd index e1cc3048..ae653420 100644 --- a/man/get_model_lineage.Rd +++ b/man/get_model_lineage.Rd @@ -1,25 +1,46 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/extendr-wrappers.R +% Please edit documentation in R/extendr-wrappers.R, R/tree-methods.R \name{get_model_lineage} \alias{get_model_lineage} -\title{Get's model lineage} +\title{Show model lineage and relationships.} \usage{ -get_model_lineage(model_dir) +get_model_lineage(model = NULL, from = NULL, to = NULL, verbose = FALSE) + +get_model_lineage(model = NULL, from = NULL, to = NULL, verbose = FALSE) } \arguments{ -\item{model_dir}{path to directory containing all models, or a hyperion_nonmem_model object -(uses the model's parent directory)} +\item{model}{Optional \code{hyperion_nonmem_model} object or model file path. +Returns the model's full lineage (ancestors and descendants). Conflicts +with \code{from}/\code{to}.} + +\item{from}{Filter the tree to this model and everything downstream. +Accepts a \code{hyperion_nonmem_model} object or a model file path.} + +\item{to}{Filter the tree to this model and everything upstream. +Accepts a \code{hyperion_nonmem_model} object or a model file path.} + +\item{verbose}{Logical; if \code{TRUE}, the returned tree carries a +\code{"verbose"} attribute that causes \code{print()} and \code{knit_print()} to render +a flat 6-column table (Model, Parent, Description, Tags, Model Hash, +Dataset Hash) instead of the tree view.} } \value{ hyperion_nonmem_tree S3 object } \description{ -Get's model lineage +With no arguments, returns the full project lineage tree. Supplying a +model path returns that model's full lineage (ancestors and descendants). +The \code{from} and \code{to} arguments filter the tree from a model downward, up +to a model, or to the slice between two models. The project is always +rooted at the directory containing \code{pharos.toml}. } \examples{ \dontrun{ -get_model_lineage("model/nonmem/") -model <- read_model("model/nonmem/run001.mod") -get_model_lineage(model) +get_model_lineage() # whole project +get_model_lineage("model/nonmem/run003.mod") # full lineage of run003 +get_model_lineage(from = "model/nonmem/run001.mod") # run001 and descendants +get_model_lineage(to = "model/nonmem/run003.mod") # run003 and ancestors +get_model_lineage(from = "model/nonmem/run001.mod", + to = "model/nonmem/run003.mod") # slice between two models } } diff --git a/man/get_model_parameter_info.Rd b/man/get_model_parameter_info.Rd index b773a876..f7ed6b34 100644 --- a/man/get_model_parameter_info.Rd +++ b/man/get_model_parameter_info.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/comments-parsing.R +% Please edit documentation in R/parameter-info.R \name{get_model_parameter_info} \alias{get_model_parameter_info} \title{Extract all parameter comments from a model as ModelComments object} @@ -27,92 +27,12 @@ For model objects sourced from \code{.mod}/\code{.ctl} files: \item otherwise (\code{"not_run"}/\code{"running"}), metadata is read from the model file } } -\section{Comment Parsing Modes}{ +\section{Comment Parsing}{ -The parsing behavior is controlled by the \code{pharos.toml} configuration file. -In the \verb{[nonmem.comments]} section, set \code{type = "type1"} to enable structured -type1 comment parsing. If this setting is absent or set to any other value, -raw comment parsing is used (the default). - -\strong{type1 mode}: Expects comments in a structured format with explicit field -delimiters. This mode provides more precise extraction but requires comments -to follow the type1 specification. - -\strong{raw mode} (default): Flexibly parses parameter names, units, and -descriptions from free-form comment text. More forgiving but may be less -precise for complex comment structures. -} - -\section{Raw Comment Formats (default)}{ - -Applies to text after \verb{;} on lines within \verb{$THETA}, \verb{$OMEGA}, and \verb{$SIGMA} -blocks. - -Parsing pipeline in raw mode: -extract transform -> strip prefix -> extract unit -> extract name. - -\strong{THETA/SIGMA} - -General form: -\verb{[PREFIX] NAME [(UNIT)] [TRANSFORM_SEP TRANSFORM]} - -Common accepted examples: -\itemize{ -\item \code{CL} -\item \code{CL (L/HR)} -\item \code{CL [L/HR]} -\item \verb{CL ;exp} -\item \code{CL :LOG} -\item \verb{CL (L/HR) ;exp} -\item \verb{THETA1 CL (L/HR) ;exp} -\item \verb{1: CL (L/HR) ;exp} -} - -Notes: -\itemize{ -\item Prefixes are case-insensitive; colon after prefix is optional. -\item Numeric prefixes like \code{1}, \verb{1:}, \verb{1-}, \code{1.} are accepted. -\item Units are read from \verb{()} or \verb{[]}, and can appear anywhere in the string. -\item Colon transform form requires leading whitespace (e.g., \code{CL :EXP}). -\item THETA strips trailing punctuation from extracted name tokens; SIGMA -currently does not. -\item SIGMA also supports unit in transform segment, e.g. -\verb{Name ;Transform (unit)}. -} - -\strong{OMEGA} - -General form: -\verb{[PREFIX] NAME_PART [THETA_REF] [TRANSFORM_SEP TRANSFORM]} - -Common accepted examples: -\itemize{ -\item \code{IIV-CL :EXP} -\item \verb{IIV CL ;exp} -\item \verb{IIV on CL ;exp} -\item \verb{Corr CL-V ;normal} -\item \verb{11: IIV CL ;exp} -\item \verb{OMEGA(1,1): IIV CL ;exp} -} - -Notes: -\itemize{ -\item \code{THETA_REF} may split on \code{-}, \code{/}, \code{:}, \verb{,} into multiple associated -thetas. -\item If full \code{THETA_REF} matches a known theta name (case-insensitive), it is -kept whole (for example, \code{CL/F}). -\item Linking words \code{on}, \code{for}, \code{of} are skipped in space-separated forms. -} - -\strong{Transform keyword mapping} (case-insensitive): -\itemize{ -\item \code{exp}, \code{log}, \code{lognormal} -> \code{LogNormal} -\item \code{logit} -> \code{Logit} -\item \code{add}, \code{adderr}, \code{additive} -> \code{AddErr} -\item \code{logadd}, \code{logadderr}, \code{logerr} -> \code{LogAddErr} -\item \code{prop}, \code{proportional} -> \code{Proportional} -\item \code{identity}, \code{normal}, \code{none} -> \code{Identity} -} +Comments are parsed by pharos according to the \verb{[nonmem.comments]} section +of \code{pharos.toml}. Set \code{type = "type1"} for strict structured comments, or +\code{type = "type2"} for a more flexible structured grammar. See pharos +documentation for accepted formats. } \seealso{ diff --git a/man/get_model_parameter_names.Rd b/man/get_model_parameter_names.Rd index 4ef8c0d6..78e87355 100644 --- a/man/get_model_parameter_names.Rd +++ b/man/get_model_parameter_names.Rd @@ -10,7 +10,7 @@ get_model_parameter_names(model) \item{model}{hyperion_nonmem_model object from read_model()} } \value{ -Named character vector with NONMEM names as names and user-friendly names as values +Named list with NONMEM names as names and user-friendly names as character values } \description{ This function extracts parameter names using the pharos typed comment parser. diff --git a/man/hyperion-package.Rd b/man/hyperion-package.Rd index 2c36ef7f..238b4850 100644 --- a/man/hyperion-package.Rd +++ b/man/hyperion-package.Rd @@ -104,8 +104,8 @@ Functions for pharos configuration: \itemize{ \item \code{\link[=init]{init()}} - Initialize pharos with config file path \item \code{\link[=get_pharos_config]{get_pharos_config()}} - Get current pharos configuration -\item \code{\link[=get_comment_type]{get_comment_type()}} - Get comment parsing mode (raw or type1) -\item \code{\link[=use_type1_comments]{use_type1_comments()}} - Configure pharos.toml for type1 comment parsing +\item \code{\link[=get_comment_type]{get_comment_type()}} - Get comment parsing mode (type1 or type2) +\item \code{\link[=use_comments]{use_comments()}} - Configure pharos.toml comment parsing type } } diff --git a/man/knit_print.hyperion_nonmem_tree.Rd b/man/knit_print.hyperion_nonmem_tree.Rd index ae65b663..9e3ac94c 100644 --- a/man/knit_print.hyperion_nonmem_tree.Rd +++ b/man/knit_print.hyperion_nonmem_tree.Rd @@ -7,7 +7,9 @@ \method{knit_print}{hyperion_nonmem_tree}(x, ...) } \arguments{ -\item{x}{A hyperion_nonmem_tree object} +\item{x}{A hyperion_nonmem_tree object. If the object carries a \code{"verbose"} +attribute (set by \code{get_model_lineage(verbose = TRUE)}), the flat table +layout is rendered instead of the tree view.} \item{...}{Additional arguments (ignored)} } diff --git a/man/map_parameterization.Rd b/man/map_parameterization.Rd new file mode 100644 index 00000000..b6814128 --- /dev/null +++ b/man/map_parameterization.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extendr-wrappers.R +\name{map_parameterization} +\alias{map_parameterization} +\title{Canonicalize a parameterization alias to its PascalCase form.} +\usage{ +map_parameterization(raw) +} +\arguments{ +\item{raw}{Parameterization alias (e.g. \code{"EXP"}, \code{"lognormal"}, \code{"PROP"}).} +} +\value{ +Canonical name (\code{"LogNormal"}, \code{"Proportional"}, ...) or \code{NA_character_} +if \code{raw} is not a recognized alias. +} +\description{ +Canonicalize a parameterization alias to its PascalCase form. +} +\keyword{internal} diff --git a/man/print.hyperion_nonmem_tree.Rd b/man/print.hyperion_nonmem_tree.Rd index 65febd7a..9eb4d0ff 100644 --- a/man/print.hyperion_nonmem_tree.Rd +++ b/man/print.hyperion_nonmem_tree.Rd @@ -4,11 +4,14 @@ \alias{print.hyperion_nonmem_tree} \title{Print Method for Hyperion Tree Objects} \usage{ -\method{print}{hyperion_nonmem_tree}(x, ...) +\method{print}{hyperion_nonmem_tree}(x, verbose = isTRUE(attr(x, "verbose")), ...) } \arguments{ \item{x}{A hyperion_nonmem_tree object} +\item{verbose}{Logical; if \code{TRUE}, render a flat table with model, +description, and tags columns instead of the tree view.} + \item{...}{Additional arguments (currently unused)} } \value{ @@ -16,5 +19,7 @@ Invisibly returns the input object } \description{ Displays a hyperion_nonmem_tree in a readable tree format using cli::tree(). -Shows the hierarchical relationships between models with Unicode tree characters. +Shows the hierarchical relationships between models with Unicode tree +characters. The default compact view shows only the model description; pass +\code{verbose = TRUE} for a flat table that also includes tags. } diff --git a/man/read_model.Rd b/man/read_model.Rd index 5708d791..973ffe22 100644 --- a/man/read_model.Rd +++ b/man/read_model.Rd @@ -2,18 +2,23 @@ % Please edit documentation in R/extendr-wrappers.R \name{read_model} \alias{read_model} -\title{Gets model object} +\title{Read a NONMEM model from a .mod or .ctl file} \usage{ read_model(path) } \arguments{ -\item{path}{path to mod or ctl file.} +\item{path}{path to a .mod or .ctl file.} } \value{ -hyperion_nonmem_model S3 object with \code{model_source} and \code{run_status} attributes +A \code{hyperion_nonmem_model} S3 object with attributes: +\itemize{ +\item \code{filename}: the model stem (e.g. \code{"run001"}) +\item \code{model_source}: path to the source file, relative to the pharos config dir +\item \code{run_status}: \code{"run"}, \code{"running"}, or \code{"not_run"} determined from output files on disk +} } \description{ -Gets model object +Read a NONMEM model from a .mod or .ctl file } \examples{ \dontrun{ diff --git a/man/read_model_from_lst.Rd b/man/read_model_from_lst.Rd index 9cb6c081..1dcdf133 100644 --- a/man/read_model_from_lst.Rd +++ b/man/read_model_from_lst.Rd @@ -2,17 +2,22 @@ % Please edit documentation in R/extendr-wrappers.R \name{read_model_from_lst} \alias{read_model_from_lst} -\title{Gets model object from lst file (internal)} +\title{Read a model from an .lst file (internal)} \usage{ read_model_from_lst(path) } \arguments{ -\item{path}{path to lst file, model output directory, or metadata.json file.} +\item{path}{path to an .lst file, model output directory, or metadata.json file.} } \value{ -hyperion_nonmem_model S3 object with \code{model_source} attribute for the source file +A \code{hyperion_nonmem_model} S3 object with attributes: +\itemize{ +\item \code{filename}: the model stem (e.g. \code{"run001"}) +\item \code{model_source}: path to the source file, relative to the pharos config dir +\item \code{run_status}: \code{"run"}, \code{"running"}, or \code{"not_run"} determined from output files on disk +} } \description{ -Gets model object from lst file (internal) +Read a model from an .lst file (internal) } \keyword{internal} diff --git a/man/set_metadata_file.Rd b/man/set_metadata_file.Rd index 35414746..cb4cdc3d 100644 --- a/man/set_metadata_file.Rd +++ b/man/set_metadata_file.Rd @@ -4,7 +4,13 @@ \alias{set_metadata_file} \title{Creates a metadata file for a NONMEM model} \usage{ -set_metadata_file(model_path, description = NULL, tags = NULL, based_on = NULL) +set_metadata_file( + model_path, + description = NULL, + tags = NULL, + based_on = NULL, + copied_from = NULL +) } \arguments{ \item{model_path}{Path to the NONMEM model file, or a hyperion_nonmem_model object (required)} @@ -14,6 +20,8 @@ set_metadata_file(model_path, description = NULL, tags = NULL, based_on = NULL) \item{tags}{Character vector of tags to categorize or label the model} \item{based_on}{Character vector of model names/paths that this model is based on} + +\item{copied_from}{Optional model name/path this model was mechanically copied from} } \value{ Returns invisibly after creating the metadata file diff --git a/man/use_comments.Rd b/man/use_comments.Rd new file mode 100644 index 00000000..567a0685 --- /dev/null +++ b/man/use_comments.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pharos-config.R +\name{use_comments} +\alias{use_comments} +\alias{use_type1_comments} +\title{Set comment parsing type in pharos.toml} +\usage{ +use_comments(type = c("type1", "type2"), path = NULL) + +use_type1_comments(path = NULL) +} +\arguments{ +\item{type}{Comment parsing type. One of \code{"type1"} (strict structured) or +\code{"type2"} (flexible structured grammar).} + +\item{path}{Path to pharos.toml. If NULL, finds it automatically.} +} +\value{ +The path to the modified pharos.toml file (invisibly). +} +\description{ +Writes \verb{type = } under the \verb{[nonmem.comments]} section of +\code{pharos.toml}. +} +\examples{ +\dontrun{ +use_comments("type2") +} +} diff --git a/man/use_type1_comments.Rd b/man/use_type1_comments.Rd deleted file mode 100644 index 5c3b86d8..00000000 --- a/man/use_type1_comments.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/pharos-config.R -\name{use_type1_comments} -\alias{use_type1_comments} -\title{Set comment type to type1 in pharos.toml} -\usage{ -use_type1_comments(path = NULL) -} -\arguments{ -\item{path}{Path to pharos.toml. If NULL, finds it automatically.} -} -\value{ -The path to the modified pharos.toml file (invisibly). -} -\description{ -Modifies the pharos.toml configuration file to use type1 comment parsing. -This is useful when NONMEM control streams use the type1 comment format. -} -\examples{ -\dontrun{ -use_type1_comments() -} -} diff --git a/pharos.toml b/pharos.toml deleted file mode 100644 index f18e919b..00000000 --- a/pharos.toml +++ /dev/null @@ -1,29 +0,0 @@ -[nonmem] -clean_level = 1 -default_version = "nm760" -files_to_copy = [] - -[nonmem.options] -prsame = false -prcompile = false -prdefault = false -tprdefault = false -background = false -nobuild = false -maxlim = 2 - -[nonmem.versions] -nm760 = "/opt/nonmem/nm760" - -[nonmem.parallel] -mpiexec_path = "/opt/homebrew/bin/mpiexec" -enabled = false -num_cpus = 4 -timeout = 2147483647 - -[nonmem.comments] -error_on_invalid = false - -[nonmem.summary] -high_correlation_threshold = 0.95 -high_condition_threshold = 1000 diff --git a/rproject.toml b/rproject.toml index 6e25037a..64f94fef 100644 --- a/rproject.toml +++ b/rproject.toml @@ -12,7 +12,7 @@ repositories = [ dependencies = [ "devtools", - { name = "rextendr", git = "https://github.com/extendr/rextendr", branch = "main" }, + { name = "rextendr", git = "https://github.com/extendr/rextendr", tag = "v0.5.0" }, { name = "covr", install_suggestions = true }, #{ name = "starlightr", git = "https://github.com/a2-ai/starlightr", branch = "main" }, diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index c77da467..4dc102e9 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -46,15 +46,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -91,9 +91,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -141,13 +141,14 @@ dependencies = [ [[package]] name = "config" version = "0.1.0" -source = "git+https://github.com/a2-ai/pharos?branch=main#6ee38159248a7fc039bb46aee46e58a55b9cc9d0" +source = "git+https://github.com/a2-ai/pharos?tag=v0.5.0#8a55a9a2babd81105617b8a1e178b48847a03d9f" dependencies = [ "anyhow", "fs-err", "glob", "jiff", "log", + "nonmem-parser", "serde", "tera", "toml", @@ -282,8 +283,9 @@ dependencies = [ [[package]] name = "extendr-api" -version = "0.8.1" -source = "git+https://github.com/extendr/extendr?branch=main#8aafb67b77cd112dd6192ff05efa3bade7ea8e8b" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803569de0d273b4bf281871046a7d63a23cc12776bdb5b63de5c1e81aae30728" dependencies = [ "extendr-ffi", "extendr-macros", @@ -295,13 +297,15 @@ dependencies = [ [[package]] name = "extendr-ffi" -version = "0.8.1" -source = "git+https://github.com/extendr/extendr?branch=main#8aafb67b77cd112dd6192ff05efa3bade7ea8e8b" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba82ddd48e85202654997b81e4b1d39c0c54b5dcd7cae92705f807bf528efcf" [[package]] name = "extendr-macros" -version = "0.8.1" -source = "git+https://github.com/extendr/extendr?branch=main#8aafb67b77cd112dd6192ff05efa3bade7ea8e8b" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8fad8d2a0d0651b1947042cf3a8beddc73d39cec3485b200fdfd24cb3bb6aa" dependencies = [ "lazy_static", "proc-macro2", @@ -321,6 +325,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -336,6 +346,30 @@ dependencies = [ "autocfg", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -423,9 +457,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -475,12 +509,14 @@ dependencies = [ name = "hyperion-nonmem" version = "0.1.0" dependencies = [ + "anyhow", "config", "extendr-api", "fs-err", "hyperion-core", "insta", "nonmem", + "nonmem-parser", "serde", "tempfile", ] @@ -550,7 +586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -577,9 +613,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -592,9 +628,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -618,10 +654,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -640,9 +678,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -662,6 +700,38 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + [[package]] name = "memchr" version = "2.8.0" @@ -671,18 +741,17 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nonmem" version = "0.1.0" -source = "git+https://github.com/a2-ai/pharos?branch=main#6ee38159248a7fc039bb46aee46e58a55b9cc9d0" +source = "git+https://github.com/a2-ai/pharos?tag=v0.5.0#8a55a9a2babd81105617b8a1e178b48847a03d9f" dependencies = [ "anyhow", "blake3", "config", - "distrs", "fs-err", "glob", "jiff", "log", + "nonmem-parser", "num_cpus", - "rand 0.9.2", "rayon", "regex", "serde", @@ -694,6 +763,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "nonmem-parser" +version = "0.1.0" +source = "git+https://github.com/a2-ai/pharos?tag=v0.5.0#8a55a9a2babd81105617b8a1e178b48847a03d9f" +dependencies = [ + "anyhow", + "distrs", + "fs-err", + "log", + "logos", + "rand 0.9.4", + "regex", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -809,7 +893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -821,6 +905,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -829,9 +919,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -887,9 +977,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -898,9 +988,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -946,9 +1036,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1035,7 +1125,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/a2-ai/pharos?branch=main#6ee38159248a7fc039bb46aee46e58a55b9cc9d0" +source = "git+https://github.com/a2-ai/pharos?tag=v0.5.0#8a55a9a2babd81105617b8a1e178b48847a03d9f" dependencies = [ "anyhow", "config", @@ -1149,9 +1239,15 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slug" @@ -1201,7 +1297,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.8.5", + "rand 0.8.6", "regex", "serde", "serde_json", @@ -1239,7 +1335,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -1250,9 +1346,9 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -1281,7 +1377,7 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/a2-ai/pharos?branch=main#6ee38159248a7fc039bb46aee46e58a55b9cc9d0" +source = "git+https://github.com/a2-ai/pharos?tag=v0.5.0#8a55a9a2babd81105617b8a1e178b48847a03d9f" dependencies = [ "anyhow", "fs-err", @@ -1314,11 +1410,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -1327,14 +1423,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -1345,9 +1441,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1355,9 +1451,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -1368,9 +1464,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -1503,9 +1599,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -1516,6 +1612,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index aa57b0b5..334bd8f9 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -35,12 +35,13 @@ serde = { workspace = true } [workspace.dependencies] # R Integration -extendr-api = { git = "https://github.com/extendr/extendr", branch = "main", features = ["serde"] } +extendr-api = { version = "0.9.0", features = ["serde"] } # Pharos components -nonmem = { git = "https://github.com/a2-ai/pharos", package = "nonmem", branch = "main" } -config = { git = "https://github.com/a2-ai/pharos", package = "config", branch = "main" } -scheduler = { git = "https://github.com/a2-ai/pharos", package = "scheduler", branch = "main" } +nonmem = { git = "https://github.com/a2-ai/pharos", package = "nonmem", tag = "v0.5.0" } +nmparser = { git = "https://github.com/a2-ai/pharos", package = "nonmem-parser", tag = "v0.5.0" } +config = { git = "https://github.com/a2-ai/pharos", package = "config", tag = "v0.5.0" } +scheduler = { git = "https://github.com/a2-ai/pharos", package = "scheduler", tag = "v0.5.0" } # Core utilities anyhow = "1.0.100" diff --git a/src/rust/core/src/lib.rs b/src/rust/core/src/lib.rs index 1222b21b..e9b3a506 100644 --- a/src/rust/core/src/lib.rs +++ b/src/rust/core/src/lib.rs @@ -34,12 +34,29 @@ macro_rules! extendr_err { }; } +/// Name of the R option that overrides pharos's CWD-walk for the project +/// root. When set to a non-empty string, `find_config_dir` returns that +/// path verbatim — `pharos.toml` is not required to live there. Useful +/// for tests and scripts that don't want hyperion to depend on CWD. +pub const CONFIG_DIR_OPTION: &str = "hyperion.config_dir"; + pub fn find_config_dir() -> Result> { + if let Some(path) = config_dir_from_option() { + return Ok(Some(path)); + } pharos_find_config_dir().map_to_extendr_err("Failed to find config dir") } +/// Read the `hyperion.config_dir` R option. Returns `None` when the +/// option is unset, NULL, or an empty string. +fn config_dir_from_option() -> Option { + let opt = call!("getOption", CONFIG_DIR_OPTION).ok()?; + let s = opt.as_str().filter(|s| !s.is_empty())?; + Some(PathBuf::from(s)) +} + #[extendr] -pub fn set_panic_message() { +pub fn silence_panic_output() { std::panic::set_hook(Box::new(|_| {})); } @@ -59,6 +76,6 @@ pub fn find_pharos_config_file() -> Result { extendr_module! { mod hyperion_core; - fn set_panic_message; + fn silence_panic_output; fn find_pharos_config_file; } diff --git a/src/rust/nonmem/Cargo.toml b/src/rust/nonmem/Cargo.toml index 4613af79..07edaf8a 100644 --- a/src/rust/nonmem/Cargo.toml +++ b/src/rust/nonmem/Cargo.toml @@ -10,9 +10,11 @@ hyperion-core = { path = "../core" } # Pharos components nonmem = { workspace = true } +nmparser = { workspace = true } config = { workspace = true } # Core utilities +anyhow = { workspace = true } fs-err = { workspace = true } serde = { workspace = true } diff --git a/src/rust/nonmem/src/model/check.rs b/src/rust/nonmem/src/model/check.rs index 7bcd6a98..553ebcbd 100644 --- a/src/rust/nonmem/src/model/check.rs +++ b/src/rust/nonmem/src/model/check.rs @@ -1,9 +1,12 @@ use extendr_api::Result; use extendr_api::prelude::*; -use hyperion_core::ResultExt; +use extendr_api::serializer::to_robj; +use hyperion_core::{OptionExt, ResultExt}; // pharos config and nonmem crates -use nonmem::check_model; +use nonmem::{check_dataset as nonmem_check_dataset, check_model}; + +use extendr_api::deserializer::from_robj; use crate::utils::{load_nonmem_config, path_from_robj}; use hyperion_core::extendr_err; @@ -45,8 +48,32 @@ pub fn check_model_wrap(model_path: Robj) -> Result { Ok(res.exit_code) } +/// Checks model dataset +/// +/// @param model hyperion_nonmem_model object from `read_model` +/// +/// @return Dataset check results +/// @export +#[extendr] +pub fn check_dataset(model: Robj) -> Result { + let model_path = path_from_robj(&model, false)?; + let model_dir = model_path + .parent() + .ok_or_extendr_err("Could not determine model directory")?; + + let model = from_robj(&model)?; + let dataset = nonmem_check_dataset(&model, model_dir).map_to_extendr_err("")?; + + let mut 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) +} + extendr_module! { mod check; fn check_model_wrap; + fn check_dataset; } diff --git a/src/rust/nonmem/src/model/comment_info.rs b/src/rust/nonmem/src/model/comment_info.rs new file mode 100644 index 00000000..4a5cd818 --- /dev/null +++ b/src/rust/nonmem/src/model/comment_info.rs @@ -0,0 +1,212 @@ +use std::collections::BTreeMap; + +use extendr_api::Result; +use extendr_api::deserializer::from_robj; +use extendr_api::prelude::*; +use extendr_api::serializer::to_robj; + +use serde::Serialize; + +use nmparser::{ + Model, OmegaSigmaEntry, OmegaSigmaParam, ParameterOrdering, ParsedOmegaComment, + ParsedRaneffComment, ParsedSigmaComment, ParsedThetaComment, Transform, Type1Theta, +}; + +use crate::model::parameters::compare_param_names; +use hyperion_core::ResultExt; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct ThetaCommentInfo { + pub name: Option, + pub unit: Option, + pub parameterization: Option, +} + +impl From<&ParsedThetaComment> for ThetaCommentInfo { + fn from(parsed: &ParsedThetaComment) -> Self { + match parsed { + ParsedThetaComment::Type1(Type1Theta::WithUnit { + parameter, + unit, + parametrization, + }) => ThetaCommentInfo { + name: Some(parameter.clone()), + unit: Some(unit.clone()), + parameterization: parametrization.as_deref().and_then(map_parameterization), + }, + ParsedThetaComment::Type1(Type1Theta::Covariate { parameter }) => ThetaCommentInfo { + name: Some(parameter.clone()), + unit: None, + parameterization: None, + }, + ParsedThetaComment::Type1(Type1Theta::Type { + typ, + parameterization, + }) => ThetaCommentInfo { + name: Some(typ.clone()), + unit: None, + parameterization: map_parameterization(parameterization), + }, + ParsedThetaComment::Type2(t) => ThetaCommentInfo { + name: Some(t.name.clone()), + unit: t.unit.clone(), + parameterization: t.parameterization.as_ref().map(ToString::to_string), + }, + } + } +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct OmegaCommentInfo { + pub name: Option, + pub raw_name: Option, + pub associated_theta: Vec, + pub parameterization: Option, +} + +impl From<&ParsedOmegaComment> for OmegaCommentInfo { + fn from(parsed: &ParsedOmegaComment) -> Self { + match parsed { + ParsedOmegaComment::Type1(o) => OmegaCommentInfo { + name: parsed.name(), + raw_name: Some(o.name.clone()), + associated_theta: vec![o.theta_name.clone()], + parameterization: map_parameterization(&o.parameterization), + }, + ParsedOmegaComment::Type2(o) => OmegaCommentInfo { + name: parsed.name(), + raw_name: Some(o.name.clone()), + associated_theta: o.raw_theta_refs.clone(), + parameterization: o.parameterization.as_ref().map(ToString::to_string), + }, + } + } +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct SigmaCommentInfo { + pub name: Option, + pub unit: Option, + pub parameterization: Option, +} + +impl From<&ParsedSigmaComment> for SigmaCommentInfo { + fn from(parsed: &ParsedSigmaComment) -> Self { + match parsed { + ParsedSigmaComment::Type1(s) => SigmaCommentInfo { + name: Some(s.name.clone()), + unit: None, + parameterization: s.parameterization.as_deref().and_then(map_parameterization), + }, + ParsedSigmaComment::Type2(s) => SigmaCommentInfo { + name: Some(s.name.clone()), + unit: s.unit.clone(), + parameterization: s.parameterization.as_ref().map(ToString::to_string), + }, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ModelCommentInfo { + pub thetas: Vec<(String, ThetaCommentInfo)>, + pub omegas: Vec<(String, OmegaCommentInfo)>, + pub sigmas: Vec<(String, SigmaCommentInfo)>, +} + +fn build_theta_info(parsed: Option<&ParsedThetaComment>) -> ThetaCommentInfo { + parsed.map(ThetaCommentInfo::from).unwrap_or_default() +} + +fn build_omega_info(param: &OmegaSigmaParam) -> OmegaCommentInfo { + let inner = match param.parsed_comment.as_ref() { + Some(ParsedRaneffComment::Omega(o)) => Some(o), + Some(ParsedRaneffComment::Sigma(_)) => { + panic!("pharos invariant: omega block parameter has Sigma parsed comment") + } + None => None, + }; + + inner.map(OmegaCommentInfo::from).unwrap_or_default() +} + +fn build_sigma_info(param: &OmegaSigmaParam) -> SigmaCommentInfo { + let inner = match param.parsed_comment.as_ref() { + Some(ParsedRaneffComment::Sigma(s)) => Some(s), + Some(ParsedRaneffComment::Omega(_)) => { + panic!("pharos invariant: sigma block parameter has Omega parsed comment") + } + None => None, + }; + + inner.map(SigmaCommentInfo::from).unwrap_or_default() +} + +fn sort_entries(entries: BTreeMap) -> Vec<(String, T)> { + let mut out: Vec<(String, T)> = entries.into_iter().collect(); + out.sort_by(|a, b| compare_param_names(&a.0, &b.0)); + out +} + +/// Walk a model's parameters and produce per-parameter comment info. +pub fn build(model: &Model) -> anyhow::Result { + let mut thetas: BTreeMap = BTreeMap::new(); + for (i, theta) in model.thetas.iter().enumerate() { + let key = format!("THETA{}", i + 1); + thetas.insert(key, build_theta_info(theta.parsed_comment.as_ref())); + } + + let omega_entries: Vec = + model.get_omega_parameters(ParameterOrdering::RowMajor)?; + let mut omegas: BTreeMap = BTreeMap::new(); + for entry in omega_entries { + let info = build_omega_info(&entry.parameter); + omegas.insert(entry.param_name, info); + } + + let sigma_entries: Vec = + model.get_sigma_parameters(ParameterOrdering::RowMajor)?; + let mut sigmas: BTreeMap = BTreeMap::new(); + for entry in sigma_entries { + let info = build_sigma_info(&entry.parameter); + sigmas.insert(entry.param_name, info); + } + + Ok(ModelCommentInfo { + thetas: sort_entries(thetas), + omegas: sort_entries(omegas), + sigmas: sort_entries(sigmas), + }) +} + +/// Build per-parameter comment info from a model object (internal) +/// +/// @param model hyperion_nonmem_model object from read_model() +/// +/// @return list with `thetas`, `omegas`, `sigmas` entries; each is a list of +/// length-2 lists `(coordinate, info)` in numeric coordinate order. +/// @keywords internal +#[extendr] +pub fn get_model_comment_info(model: Robj) -> Result { + let model = from_robj(&model)?; + let info = build(&model).map_to_extendr_err("Failed to build comment info")?; + to_robj(&info).map_to_extendr_err("Failed to serialize comment info") +} + +/// Canonicalize a parameterization alias to its PascalCase form. +/// +/// @param raw Parameterization alias (e.g. `"EXP"`, `"lognormal"`, `"PROP"`). +/// @return Canonical name (`"LogNormal"`, `"Proportional"`, ...) or `NA_character_` +/// if `raw` is not a recognized alias. +/// @keywords internal +#[extendr] +pub fn map_parameterization(raw: &str) -> Option { + raw.parse::().ok().map(|t| t.to_string()) +} + +extendr_module! { + mod comment_info; + + fn get_model_comment_info; + fn map_parameterization; +} diff --git a/src/rust/nonmem/src/model/copy.rs b/src/rust/nonmem/src/model/copy.rs index e491c154..c8ac290e 100644 --- a/src/rust/nonmem/src/model/copy.rs +++ b/src/rust/nonmem/src/model/copy.rs @@ -1,11 +1,12 @@ use extendr_api::Result; use extendr_api::prelude::*; +use fs_err as fs; use nonmem::copy::UpdateType; -use nonmem::{CopyOptions, copy_model}; +use nonmem::{CopyOptions, Model, copy_model}; use std::path::{Path, PathBuf}; -use crate::utils::path_from_robj; +use crate::utils::{path_from_robj, resolve_ext_path}; use hyperion_core::{OptionExt, ResultExt, extendr_err}; // This should move to Option @@ -91,6 +92,8 @@ fn parse_update_robj(update: Robj) -> Result> { /// Examples: "THETA1" or c("THETA1") /// @param seed integer for random number generator seed to ensure reproducible jittering /// @param description Description of model in metadata file +/// @param based_on Character vector of model names/paths that this model is based on +/// @param tags Character vector of tags to attach to the model in metadata /// @param no_metadata boolean, if true, does not create metadatafile, default FALSE /// /// @return path to new model file (invisible) todo @@ -112,7 +115,9 @@ pub fn copy_model_wrap( // This should move to Option #[extendr(default = "NULL")] jitter_excluded: Option<&Robj>, #[extendr(default = "NULL")] seed: Option, - #[extendr(default = "NULL")] description: String, + description: String, + #[extendr(default = "NULL")] based_on: Option>, + #[extendr(default = "NULL")] tags: Option>, #[extendr(default = "FALSE")] no_metadata: bool, ) -> Result<()> { // Parse input parameters @@ -130,6 +135,8 @@ pub fn copy_model_wrap( seed, jitter_excluded: jitter_excluded_parsed, description, + based_on: based_on.unwrap_or_default(), + tags: tags.unwrap_or_default(), no_metadata, }; @@ -158,17 +165,19 @@ pub fn copy_model_wrap( let ext_path = match &ext_file { Some(path) => PathBuf::from(path), None => { - // Default: run001.mod -> run001/run001.ext let model_stem = from_path .file_stem() .ok_or_extendr_err("Could not determine model file stem")? - .to_string_lossy(); - - from_path + .to_string_lossy() + .to_string(); + let run_dir = from_path .parent() .ok_or_extendr_err("Could not determine parent directory")? - .join(&*model_stem) - .join(format!("{}.ext", model_stem)) + .join(&model_stem); + let content = fs::read_to_string(&from_path).map_to_extendr_err("")?; + let model = Model::parse(&content) + .map_err(|_| extendr_err!("Failed to parse model: {}", from_path.display()))?; + resolve_ext_path(&model, &run_dir, &model_stem) } }; diff --git a/src/rust/nonmem/src/model/lineage.rs b/src/rust/nonmem/src/model/lineage.rs index f4b46da9..4f9cd433 100644 --- a/src/rust/nonmem/src/model/lineage.rs +++ b/src/rust/nonmem/src/model/lineage.rs @@ -2,13 +2,15 @@ use extendr_api::Result; use extendr_api::prelude::*; use extendr_api::serializer::to_robj; +use fs_err as fs; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use std::path::Path; use nonmem::{LineageTree, ModelMetadata, OutputFileHash, RunEndFile, RunStartFile}; -use crate::utils::{path_from_robj, to_config_relative}; -use hyperion_core::{OptionExt, ResultExt}; +use crate::utils::path_from_robj; +use hyperion_core::{OptionExt, ResultExt, extendr_err, find_config_dir}; /// R-compatible version of RunEndFile with u128 -> f64 conversion #[derive(Debug, Serialize, Deserialize, Clone)] @@ -21,20 +23,30 @@ pub struct RRunEndFile { pub output_files_hashes: Vec, } -/// R-compatible version of LineageTree -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct RLineageTree { - pub nodes: HashMap, - pub metadata: HashMap)>, - pub source_dir: String, +/// Run-specific info (start file + optional end file) for one model. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunInfo { + pub start: RunStartFile, + pub end: Option, } -impl RLineageTree { - /// Set the source directory for this lineage tree - pub fn with_source_dir(mut self, source_dir: String) -> Self { - self.source_dir = source_dir; - self - } +/// One entry in the lineage tree. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LineageNode { + pub name: String, + pub model: ModelMetadata, + pub run: Option, +} + +/// R-compatible version of LineageTree. +/// +/// `nodes` is an ordered `Vec` so iteration order is deterministic and +/// matches pharos's `topological_order` (parents before children, +/// alphabetical tie-breaks). Each entry bundles the model metadata and the +/// optional run info, so R doesn't have to navigate parallel maps. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct RLineageTree { + pub nodes: Vec, } impl From for RRunEndFile { @@ -52,61 +64,110 @@ impl From for RRunEndFile { impl From for RLineageTree { fn from(lineage: LineageTree) -> Self { - let r_metadata = lineage - .metadata + let all_keys: HashSet = lineage.nodes.keys().cloned().collect(); + let chain = lineage.topological_order(all_keys); + let mut project_metadata = lineage.metadata; + let nodes = chain .into_iter() - .map(|(key, (start_file, opt_end_file))| { - let r_end_file = opt_end_file.map(|end_file| end_file.into()); - (key, (start_file, r_end_file)) + .map(|(name, model)| { + let run = project_metadata + .remove(&name) + .map(|(start, end_opt)| RunInfo { + start, + end: end_opt.map(Into::into), + }); + LineageNode { name, model, run } }) .collect(); - - RLineageTree { - nodes: lineage.nodes, - metadata: r_metadata, - source_dir: String::new(), // Set by caller via with_source_dir() - } + RLineageTree { nodes } } } -/// Get's model lineage +/// Show model lineage and relationships. +/// +/// With no arguments, returns the full project lineage tree. Supplying a +/// model path returns that model's full lineage (ancestors and descendants). +/// The `from` and `to` arguments filter the tree from a model downward, up +/// to a model, or to the slice between two models. The project is always +/// rooted at the directory containing `pharos.toml`. /// -/// @param model_dir path to directory containing all models, or a hyperion_nonmem_model object -/// (uses the model's parent directory) +/// @param model Optional `hyperion_nonmem_model` object or model file path. +/// Returns the model's full lineage (ancestors and descendants). Conflicts +/// with `from`/`to`. +/// @param from Filter the tree to this model and everything downstream. +/// Accepts a `hyperion_nonmem_model` object or a model file path. +/// @param to Filter the tree to this model and everything upstream. +/// Accepts a `hyperion_nonmem_model` object or a model file path. /// /// @return hyperion_nonmem_tree S3 object /// @export /// /// @examples \dontrun{ -/// get_model_lineage("model/nonmem/") -/// model <- read_model("model/nonmem/run001.mod") -/// get_model_lineage(model) +/// get_model_lineage() # whole project +/// get_model_lineage("model/nonmem/run003.mod") # full lineage of run003 +/// get_model_lineage(from = "model/nonmem/run001.mod") # run001 and descendants +/// get_model_lineage(to = "model/nonmem/run003.mod") # run003 and ancestors +/// get_model_lineage(from = "model/nonmem/run001.mod", +/// to = "model/nonmem/run003.mod") # slice between two models /// } #[extendr] -pub fn get_model_lineage(model_dir: Robj) -> Result { - let path = path_from_robj(&model_dir, false)?; - // If it's a file, use parent directory; if directory, use as-is - let model_dir = if path.is_file() { - path.parent() - .ok_or_extendr_err("Could not determine model directory")? - .to_path_buf() +pub fn get_model_lineage( + #[extendr(default = "NULL")] model: Option<&Robj>, + #[extendr(default = "NULL")] from: Option<&Robj>, + #[extendr(default = "NULL")] to: Option<&Robj>, +) -> Result { + // extendr passes the R NULL default as Some(&null_robj), not None. Filter + // null Robjs out so downstream `is_some()` checks reflect user intent. + let model = model.filter(|r| !r.is_null()); + let from = from.filter(|r| !r.is_null()); + let to = to.filter(|r| !r.is_null()); + + if model.is_some() && (from.is_some() || to.is_some()) { + return Err(extendr_err!("model conflicts with from/to")); + } + + let project_root = find_config_dir()? + .ok_or_extendr_err("No pharos.toml found; lineage requires a pharos project root")?; + // Canonicalize so `load_run_metadata`'s strip_prefix against each + // model_canonical_path succeeds (pharos silently drops mismatches). + let project_root = fs::canonicalize(&project_root) + .map_to_extendr_err("Failed to canonicalize project root")?; + + let lineage = LineageTree::from_project_root(project_root.clone()) + .map_to_extendr_err("Pharos failed to build lineage tree")?; + + let mut focal: Vec = Vec::new(); + let chain = if let Some(model) = model { + let model_path = path_from_robj(model, false)?; + focal.push(focal_key_for(&model_path, &project_root)); + lineage + .lineage_of(&model_path) + .map_to_extendr_err("Failed to compute model lineage")? } else { - path + let from_path = from.map(|r| path_from_robj(r, false)).transpose()?; + let to_path = to.map(|r| path_from_robj(r, false)).transpose()?; + if let Some(p) = &from_path { + focal.push(focal_key_for(p, &project_root)); + } + if let Some(p) = &to_path { + focal.push(focal_key_for(p, &project_root)); + } + lineage + .slice(from_path.as_deref(), to_path.as_deref()) + .map_to_extendr_err("Failed to slice lineage tree")? }; - // Create lineage tree from folder - let lineage = LineageTree::from_folder(&model_dir) - .map_to_extendr_err("Pharos failed to create lineage tree")?; + let lineage = filter_lineage(lineage, chain); - // Convert to R-compatible version (u128 -> f64) and attach source directory (relative to pharos.toml) - let source_dir = to_config_relative(&model_dir)?; - let r_lineage: RLineageTree = RLineageTree::from(lineage).with_source_dir(source_dir); + let r_lineage: RLineageTree = lineage.into(); - // Serialize R-compatible lineage to Robj let mut lineage_robj = to_robj(&r_lineage).map_to_extendr_err("Failed to create Robj from RLineageTree")?; - // Set S3 class + lineage_robj + .set_attrib("focal", focal) + .map_to_extendr_err("Failed to set focal attribute")?; + let hyperion_nonmem_tree = lineage_robj .set_class(["hyperion_nonmem_tree"]) .map_to_extendr_err("Failed to set class")?; @@ -114,6 +175,47 @@ pub fn get_model_lineage(model_dir: Robj) -> Result { Ok(hyperion_nonmem_tree.to_owned()) } +/// Compute the project-relative key for a user-supplied path, used to set +/// the "focal" attribute on the returned tree. Mirrors pharos's +/// `model_identity_for` resolution: canonicalize + strip the project root, +/// joining components with forward slashes. Falls back to the raw input +/// when canonicalization fails (e.g., the user passed an already-keyed +/// string). +fn focal_key_for(path: &Path, project_root: &Path) -> String { + if let Ok(canonical) = fs::canonicalize(path) + && let Ok(rel) = canonical.strip_prefix(project_root) + { + return rel + .components() + .map(|c| c.as_os_str().to_string_lossy().into_owned()) + .collect::>() + .join("/"); + } + path.to_string_lossy().into_owned() +} + +/// Trim a `LineageTree` to just the nodes in `chain`. Both `nodes` and +/// `metadata` are filtered to those keys so the downstream `From` impl sees +/// a coherent project containing only the slice. +fn filter_lineage(lineage: LineageTree, chain: Vec<(String, ModelMetadata)>) -> LineageTree { + let chain_keys: HashSet = chain.into_iter().map(|(k, _)| k).collect(); + // `project_root` is private on `LineageTree`; build via Default and + // mutate the pub fields. The downstream `From` impl only needs + // identity-keyed (string) access; it doesn't call path-based queries. + let mut filtered = LineageTree::default(); + filtered.nodes = lineage + .nodes + .into_iter() + .filter(|(k, _)| chain_keys.contains(k)) + .collect(); + filtered.metadata = lineage + .metadata + .into_iter() + .filter(|(k, _)| chain_keys.contains(k)) + .collect(); + filtered +} + extendr_module! { mod lineage; diff --git a/src/rust/nonmem/src/model/metadata.rs b/src/rust/nonmem/src/model/metadata.rs index 9a5b645d..0c7ff4ad 100644 --- a/src/rust/nonmem/src/model/metadata.rs +++ b/src/rust/nonmem/src/model/metadata.rs @@ -4,10 +4,12 @@ use extendr_api::serializer::to_robj; use nonmem::ModelMetadata; //pharos nonmem crate -use nonmem::update_metadata_file; +use nonmem::{clear_metadata_file, update_metadata_file}; use crate::utils::path_from_robj; -use hyperion_core::{ResultExt, extendr_err}; +use hyperion_core::{OptionExt, ResultExt, extendr_err}; + +const METADATA_FILENAME_SUFFIX: &str = "_metadata.json"; /// Creates a metadata file for a NONMEM model /// @@ -19,6 +21,7 @@ use hyperion_core::{ResultExt, extendr_err}; /// @param description Optional description of the model and its purpose /// @param tags Character vector of tags to categorize or label the model /// @param based_on Character vector of model names/paths that this model is based on +/// @param copied_from Optional model name/path this model was mechanically copied from /// /// @return Returns invisibly after creating the metadata file /// @export @@ -46,6 +49,7 @@ pub fn set_metadata_file( #[extendr(default = "NULL")] description: Option, #[extendr(default = "NULL")] tags: Option>, #[extendr(default = "NULL")] based_on: Option>, + #[extendr(default = "NULL")] copied_from: Option, ) -> Result<()> { if let Some(d) = &description && d.trim().is_empty() @@ -60,7 +64,7 @@ pub fn set_metadata_file( let tags = tags.unwrap_or_default(); let based_on = based_on.unwrap_or_default(); - update_metadata_file(model_path, description, tags, based_on, true) + update_metadata_file(model_path, description, tags, based_on, copied_from, true) .map_to_extendr_err("Failed to create metadata file")?; Ok(()) @@ -94,7 +98,7 @@ pub fn append_to_metadata_file( let tags = tags.unwrap_or_default(); let based_on = based_on.unwrap_or_default(); - update_metadata_file(path, description, tags, based_on, false) + update_metadata_file(path, description, tags, based_on, None, false) .map_to_extendr_err("Failed to update metadata file")?; Ok(()) @@ -139,10 +143,70 @@ pub fn load_model_metadata(model: Robj) -> Result { Ok(result.to_owned()) } +/// Clear fields in a model's metadata file +/// +/// Selectively clears the `based_on`, `copied_from`, and/or `tags` fields in +/// the metadata file associated with a model. Fields not selected are left +/// unchanged. +/// +/// @param model_path Path to the NONMEM model file, or a hyperion_nonmem_model object +/// @param based_on If TRUE, clear the based_on field. Default FALSE. +/// @param copied_from If TRUE, clear the copied_from field. Default FALSE. +/// @param tags If TRUE, clear the tags field. Default FALSE. +/// +/// @return Returns invisibly after updating the metadata file +/// @export +/// +/// @examples \dontrun{ +/// clear_metadata_file("model/nonmem/run001.mod", tags = TRUE) +/// model <- read_model("model/nonmem/run001.mod") +/// clear_metadata_file(model, based_on = TRUE, copied_from = TRUE) +/// } +#[extendr(r_name = "clear_metadata_file")] +pub fn clear_metadata_file_wrap( + model_path: Robj, + #[extendr(default = "FALSE")] based_on: bool, + #[extendr(default = "FALSE")] copied_from: bool, + #[extendr(default = "FALSE")] tags: bool, +) -> Result<()> { + let model_path = path_from_robj(&model_path, true)?; + + let model_name = model_path + .file_stem() + .ok_or_extendr_err("Could not determine model file stem")? + .to_string_lossy() + .to_string(); + let model_dir = model_path + .parent() + .ok_or_extendr_err("Could not determine model directory")? + .to_path_buf(); + let metadata_path = model_dir.join(format!("{model_name}{METADATA_FILENAME_SUFFIX}")); + + if !metadata_path.exists() { + return Err(extendr_err!( + "Metadata file does not exist: {}", + metadata_path.display() + )); + } + + clear_metadata_file( + model_name, + &model_dir, + &metadata_path, + based_on, + copied_from, + tags, + ) + .map_to_extendr_err("Failed to clear metadata file")?; + + Ok(()) +} + extendr_module! { mod metadata; fn set_metadata_file; fn append_to_metadata_file; fn load_model_metadata; + fn clear_metadata_file_wrap; } diff --git a/src/rust/nonmem/src/model/mod.rs b/src/rust/nonmem/src/model/mod.rs index 113bd691..0c2fadf8 100644 --- a/src/rust/nonmem/src/model/mod.rs +++ b/src/rust/nonmem/src/model/mod.rs @@ -1,23 +1,17 @@ use extendr_api::Result; -use extendr_api::deserializer::from_robj; use extendr_api::prelude::*; use extendr_api::serializer::to_robj; - use fs_err as fs; -use std::path::Path; - -//pharos nonmem crate -use nonmem::Model; +use nmparser::Model; use nonmem::output_files::lst; +use std::path::Path; use crate::model::run_status::determine_run_status; -use crate::utils::{ - find_output_file, from_config_relative, get_comment_type, to_config_relative, - validate_model_path, -}; -use hyperion_core::{OptionExt, ResultExt}; +use crate::utils::{find_output_file, get_comment_type, to_config_relative, validate_model_path}; +use hyperion_core::{OptionExt, ResultExt, extendr_err}; pub mod check; +pub mod comment_info; pub mod copy; pub mod lineage; pub mod metadata; @@ -25,22 +19,18 @@ pub mod parameters; pub mod run_status; pub mod summary; -/// Helper to convert Model to Robj for read_model and read_model_from_lst -/// -/// This handles comment parsing and Model -> Robj + S3 setting -fn model_to_robj(model: &mut Model, path: impl AsRef) -> Result { +/// Convert a parsed Model into a hyperion_nonmem_model Robj. +pub fn model_to_robj(model: &mut Model, path: impl AsRef) -> Result { let path = path.as_ref(); - let comment_type = get_comment_type(); - if let Some(c) = comment_type { - model.parse_comments(c); - }; + if let Some(ct) = get_comment_type() { + model.parse_comments(ct); + } - let mut model_robj = to_robj(&model).map_to_extendr_err("failed to create Robj from Model")?; + let mut model_robj = to_robj(model).map_to_extendr_err("failed to create Robj from Model")?; add_filename_attr(&mut model_robj, path)?; add_model_source_attr(&mut model_robj, path)?; - add_run_status_attr(&mut model_robj, path)?; set_model_class(&mut model_robj) } @@ -63,11 +53,31 @@ fn add_model_source_attr(model_robj: &mut Robj, path: &Path) -> Result<()> { Ok(()) } +fn set_model_class(model_robj: &mut Robj) -> Result { + let result = model_robj + .set_class(["hyperion_nonmem_model"]) + .map_to_extendr_err("Failed to set class")?; + + Ok(result.to_owned()) +} + fn add_run_status_attr(model_robj: &mut Robj, path: &Path) -> Result<()> { if let Some(ext) = path.extension().and_then(|e| e.to_str()) && (ext == "mod" || ext == "ctl" || ext == "lst") { - let run_status = determine_run_status(path)?; + let stem = path + .file_stem() + .ok_or_extendr_err("Could not determine model file stem")? + .to_string_lossy() + .to_string(); + let parent = path + .parent() + .ok_or_extendr_err("Could not determine parent directory")?; + let run_dir = match ext { + "lst" => parent.to_path_buf(), + _ => parent.join(&stem), + }; + let run_status = determine_run_status(&run_dir, &stem)?; model_robj .set_attrib("run_status", run_status.to_string().into_robj()) .map_to_extendr_err("Failed to set run_status attribute")?; @@ -75,88 +85,59 @@ fn add_run_status_attr(model_robj: &mut Robj, path: &Path) -> Result<()> { Ok(()) } - -fn set_model_class(model_robj: &mut Robj) -> Result { - let result = model_robj - .set_class(["hyperion_nonmem_model"]) - .map_to_extendr_err("Failed to set class")?; - - Ok(result.to_owned()) -} - -/// Helper function to reconstruct a pharos Model from hyperion_nonmem_model Robj -pub fn robj_to_model(model: &Robj) -> Result { - from_robj(model).map_to_extendr_err("Failed to create Model from Robj") -} - -/// Gets model object -/// -/// @param path path to mod or ctl file. -/// -/// @return hyperion_nonmem_model S3 object with `model_source` and `run_status` attributes -/// @export -/// -/// @examples \dontrun{ -/// read_model("model/nonmem/run001.mod") -/// } -#[extendr] -pub fn read_model(path: &str) -> Result { - 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")?; - let robj_model = model_to_robj(&mut model, mod_path)?; - Ok(robj_model) -} - -/// Gets model object from lst file (internal) +/// Read a model from an .lst file (internal) /// -/// @param path path to lst file, model output directory, or metadata.json file. +/// @param path path to an .lst file, model output directory, or metadata.json file. /// -/// @return hyperion_nonmem_model S3 object with `model_source` attribute for the source file +/// @return A `hyperion_nonmem_model` S3 object with attributes: +/// - `filename`: the model stem (e.g. `"run001"`) +/// - `model_source`: path to the source file, relative to the pharos config dir +/// - `run_status`: `"run"`, `"running"`, or `"not_run"` determined from output files on disk /// @keywords internal #[extendr] pub fn read_model_from_lst(path: &str) -> Result { let path = find_output_file(path, "lst")?; let mut model = lst::extract_model(&path).map_to_extendr_err("Failed to extract Model from lst file")?; - let robj_model = model_to_robj(&mut model, path)?; + let mut robj_model = model_to_robj(&mut model, &path)?; + add_run_status_attr(&mut robj_model, &path)?; Ok(robj_model) } -/// Checks model dataset +/// Read a NONMEM model from a .mod or .ctl file /// -/// @param model hyperion_nonmem_model object from `read_model` +/// @param path path to a .mod or .ctl file. /// -/// @return Dataset check results +/// @return A `hyperion_nonmem_model` S3 object with attributes: +/// - `filename`: the model stem (e.g. `"run001"`) +/// - `model_source`: path to the source file, relative to the pharos config dir +/// - `run_status`: `"run"`, `"running"`, or `"not_run"` determined from output files on disk /// @export /// /// @examples \dontrun{ -/// model <- read_model("model/nonmem/run001.mod") -/// model |> check_dataset() +/// read_model("model/nonmem/run001.mod") /// } #[extendr] -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 mut 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")?; +pub fn read_model(path: &str) -> Result { + let mod_path = validate_model_path(path)?; + let content = fs::read_to_string(&mod_path).map_to_extendr_err("")?; - Ok(robj) + let mut model = match Model::parse(&content) { + Ok(model) => model, + Err(diags) => { + let msg = diags + .iter() + .map(|d| d.render(mod_path.as_path(), &content)) + .collect::>() + .join("\n"); + return Err(extendr_err!("Failed to read model file:\n{msg}")); + } + }; + let mut robj_model = model_to_robj(&mut model, &mod_path)?; + add_run_status_attr(&mut robj_model, &mod_path)?; + + Ok(robj_model) } extendr_module! { @@ -166,10 +147,10 @@ extendr_module! { use check; use lineage; use parameters; + use comment_info; use metadata; use run_status; fn read_model; - fn check_dataset; fn read_model_from_lst; } diff --git a/src/rust/nonmem/src/model/parameters.rs b/src/rust/nonmem/src/model/parameters.rs index 34ffdbcb..37bba5d2 100644 --- a/src/rust/nonmem/src/model/parameters.rs +++ b/src/rust/nonmem/src/model/parameters.rs @@ -1,24 +1,19 @@ use extendr_api::Result; +use extendr_api::deserializer::from_robj; use extendr_api::prelude::*; -use fs_err as fs; use std::cmp::Ordering; -use std::ffi::OsStr; -use std::path::PathBuf; // pharos nonmem crate -use nonmem::{ - Model, - output_files::{ext::get_parameter_estimates, shk::ShkReader}, -}; +use nonmem::Model; +use nonmem::output_files::{ext::get_parameter_estimates, shk::ShkReader}; use crate::{ - model::robj_to_model, output_files::ext::create_ext_reader, output_files::{OMEGA, ParameterRow, ParameterRowBuilder, SIGMA, THETA, build_parameters_df}, - utils::{find_output_file, get_comment_type, path_from_robj}, + utils::{find_output_file, get_comment_type, load_model_from_input, resolve_ext_path}, }; -use hyperion_core::ResultExt; +use hyperion_core::{ResultExt, extendr_err}; /// Extract numeric indices from a parameter name for sorting. /// @@ -65,7 +60,7 @@ fn extract_param_sort_key(name: &str) -> (u32, u32, u8) { } /// Compare two parameter names for numeric sorting. -fn compare_param_names(a: &str, b: &str) -> Ordering { +pub(crate) fn compare_param_names(a: &str, b: &str) -> Ordering { let key_a = extract_param_sort_key(a); let key_b = extract_param_sort_key(b); @@ -116,32 +111,24 @@ pub fn get_parameters( ) -> Result { let ext_reader = create_ext_reader(None, None, only_method, only_last)?; - // Resolve the search path from either a string or model object - let search_path = path_from_robj(&path, false)?; - // If .ext file, use parent directory; otherwise use path as-is - let search_path = if search_path.extension() == Some(OsStr::new("ext")) { - search_path - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| PathBuf::from(".")) - } else { - search_path - }; + let loc = load_model_from_input(&path)?; - let shk_data = match find_output_file(&search_path, "shk") { + let shk_data = match find_output_file(&loc.run_dir, "shk") { Ok(p) => ShkReader.parse_file(p).unwrap_or_default(), Err(_) => Vec::new(), }; - let ext_path = find_output_file(&search_path, "ext")?; - let model_path = - find_output_file(&search_path, "mod").or_else(|_| find_output_file(&search_path, "ctl"))?; - let content = fs::read_to_string(&model_path).map_to_extendr_err("")?; - - let mut model = Model::parse(&content).map_to_extendr_err("Failed to read model file")?; + let ext_path = resolve_ext_path(&loc.model, &loc.run_dir, &loc.stem); + if !ext_path.exists() { + return Err(extendr_err!( + "Output file not found: {}", + ext_path.display() + )); + } let comment_type = get_comment_type(); - let parameter_names = model + let parameter_names = loc + .model .get_parameter_names(comment_type) .map_to_extendr_err("Failed to get model parameter names")?; @@ -220,11 +207,11 @@ pub fn get_parameters( /// /// @param model hyperion_nonmem_model object from read_model() /// -/// @return Named character vector with NONMEM names as names and user-friendly names as values +/// @return Named list with NONMEM names as names and user-friendly names as character values /// @keywords internal #[extendr] pub fn get_model_parameter_names(model: Robj) -> Result { - let mut model = robj_to_model(&model)?; + let model: Model = from_robj(&model)?; let comment_type = get_comment_type(); let parameter_names = model diff --git a/src/rust/nonmem/src/model/run_status.rs b/src/rust/nonmem/src/model/run_status.rs index d60a4659..76500948 100644 --- a/src/rust/nonmem/src/model/run_status.rs +++ b/src/rust/nonmem/src/model/run_status.rs @@ -3,9 +3,9 @@ use std::path::Path; use extendr_api::Result; use extendr_api::prelude::*; +use fs_err as fs; use hyperion_core::{OptionExt, extendr_err}; -use nonmem::output_files::ext::ExtReader; use crate::utils::{find_output_file, path_from_robj}; @@ -27,47 +27,36 @@ impl fmt::Display for RunStatus { } } -pub fn determine_run_status(path: impl AsRef) -> Result { - let path = path.as_ref(); - let stem = path - .file_stem() - .ok_or_extendr_err("Could not determine model file stem")? - .to_string_lossy() - .to_string(); - let parent = path - .parent() - .ok_or_extendr_err("Could not determine model file parent directory")?; - let run_dir = match path.extension().and_then(|e| e.to_str()) { - Some("lst") => parent.to_path_buf(), - _ => parent.join(&stem), - }; - +/// Determine the run status from on-disk outputs. +/// +/// `Run` requires the `.lst` to contain NONMEM's "Stop Time:" marker, which is +/// written at run termination regardless of whether estimation or covariance +/// succeeded. `Running` means the `.lst` exists but the marker is absent. +/// `NotRun` means neither the run directory nor the `.lst` exists. +pub fn determine_run_status(run_dir: &Path, stem: &str) -> Result { if !run_dir.exists() { return Ok(RunStatus::NotRun); } - - let ext_path = run_dir.join(format!("{}.ext", stem)); - let lst_path = run_dir.join(format!("{}.lst", stem)); - - let ext_exists = ext_path.exists(); - let lst_exists = lst_path.exists(); - - if !ext_exists && !lst_exists { + let lst_path = run_dir.join(format!("{stem}.lst")); + if !lst_path.exists() { return Ok(RunStatus::NotRun); } - - if ext_exists && lst_exists { - let ext_reader = ExtReader::default().final_estimates_only(); - if let Ok(tables) = ext_reader.parse_file(&ext_path) - && tables.iter().any(|t| !t.rows.is_empty()) - { - return Ok(RunStatus::Run); - } + if lst_indicates_completion(&lst_path) { + return Ok(RunStatus::Run); } - Ok(RunStatus::Running) } +fn lst_indicates_completion(lst_path: &Path) -> bool { + let Ok(content) = fs::read_to_string(lst_path) else { + return false; + }; + content + .lines() + .rev() + .any(|line| line.trim_start().starts_with("Stop Time:")) +} + /// Determine run status for a model path, run directory, or model object. /// /// @param input A hyperion_nonmem_model object, run directory, or model path. @@ -94,7 +83,20 @@ pub fn get_run_status(input: Robj) -> Result { } } - let status = determine_run_status(&path)?; + let stem = path + .file_stem() + .ok_or_extendr_err("Could not determine model file stem")? + .to_string_lossy() + .to_string(); + let parent = path + .parent() + .ok_or_extendr_err("Could not determine model file parent directory")?; + let run_dir = match path.extension().and_then(|e| e.to_str()) { + Some("lst") => parent.to_path_buf(), + _ => parent.join(&stem), + }; + + let status = determine_run_status(&run_dir, &stem)?; Ok(status.to_string().into_robj()) } @@ -117,54 +119,45 @@ mod tests { #[test] fn test_determine_run_status_run() { - let lst_file = test_data_dir().join("run001/run001.lst"); - let status = determine_run_status(&lst_file).unwrap(); + let run_dir = test_data_dir().join("run001"); + let status = determine_run_status(&run_dir, "run001").unwrap(); assert_eq!(status, RunStatus::Run); } #[test] fn test_determine_run_status_running() { - let lst_file = test_data_dir().join("run001-running/run001.lst"); - let status = determine_run_status(&lst_file).unwrap(); + let run_dir = test_data_dir().join("run001-running"); + let status = determine_run_status(&run_dir, "run001").unwrap(); assert_eq!(status, RunStatus::Running); } #[test] fn test_determine_run_status_not_run() { + // run_dir doesn't exist let temp_dir = TempDir::new().unwrap(); - let mod_file = temp_dir.path().join("run001.mod"); - fs::write(&mod_file, "test content").unwrap(); - - let status = determine_run_status(&mod_file).unwrap(); + let run_dir = temp_dir.path().join("run001"); + let status = determine_run_status(&run_dir, "run001").unwrap(); assert_eq!(status, RunStatus::NotRun); } #[test] fn test_determine_run_status_running_early() { - // NONMEM creates .lst before .ext during early execution + // .lst exists without "Stop Time:" => still running let temp_dir = TempDir::new().unwrap(); - let mod_file = temp_dir.path().join("run001.mod"); - fs::write(&mod_file, "test content").unwrap(); - let run_dir = temp_dir.path().join("run001"); fs::create_dir(&run_dir).unwrap(); - fs::write(run_dir.join("run001.lst"), "lst content").unwrap(); - - let status = determine_run_status(&mod_file).unwrap(); + fs::write(run_dir.join("run001.lst"), "partial lst content\n").unwrap(); + let status = determine_run_status(&run_dir, "run001").unwrap(); assert_eq!(status, RunStatus::Running); } #[test] fn test_determine_run_status_not_run_empty_run_dir() { - // Run directory exists but contains no output files + // run_dir exists but no .lst inside let temp_dir = TempDir::new().unwrap(); - let mod_file = temp_dir.path().join("run001.mod"); - fs::write(&mod_file, "test content").unwrap(); - let run_dir = temp_dir.path().join("run001"); fs::create_dir(&run_dir).unwrap(); - - let status = determine_run_status(&mod_file).unwrap(); + let status = determine_run_status(&run_dir, "run001").unwrap(); assert_eq!(status, RunStatus::NotRun); } } diff --git a/src/rust/nonmem/src/output_files/ext.rs b/src/rust/nonmem/src/output_files/ext.rs index 55faf77a..d0d45902 100644 --- a/src/rust/nonmem/src/output_files/ext.rs +++ b/src/rust/nonmem/src/output_files/ext.rs @@ -8,7 +8,7 @@ use std::path::{Path, PathBuf}; use nonmem::estimation; use nonmem::output_files::ext::{EstimationTable, ExtReader}; -use crate::utils::find_output_file; +use crate::utils::{find_output_file, load_model_from_input, resolve_ext_path, to_syntactic_name}; use hyperion_core::{OptionExt, ResultExt, extendr_err}; /// Extract .ext files from path (single file or directory) @@ -82,8 +82,12 @@ fn estimation_tables_to_dataframe(tables: Vec) -> Result return Err(extendr_err!("No tables found in ext file")); } - // Get parameter names from the first table - let param_names = tables[0].parameters.clone(); + // Get parameter names from the first table, sanitized for R syntactic use. + let param_names: Vec = tables[0] + .parameters + .iter() + .map(|n| to_syntactic_name(n)) + .collect(); let flat_data: Vec<(i32, String, Vec)> = tables .into_iter() @@ -233,20 +237,37 @@ fn fix_parameter_values(list: List, param_names: &[String]) -> Result { /// } #[extendr] pub fn read_ext_file( - path: &str, + path: Robj, #[extendr(default = "NULL")] line_prefixes: Option>, #[extendr(default = "FALSE")] parameters_only: Option, #[extendr(default = "NULL")] only_method: Option<&str>, #[extendr(default = "TRUE")] only_last: Option, ) -> Result { let ext_reader = create_ext_reader(line_prefixes, parameters_only, only_method, only_last)?; - let path = find_output_file(path, "ext")?; - - let tables = ext_reader.parse_file(path).map_to_extendr_err("")?; - + let ext_path = resolve_ext_input(&path)?; + let tables = ext_reader.parse_file(ext_path).map_to_extendr_err("")?; estimation_tables_to_dataframe(tables) } +/// Accepts a `hyperion_nonmem_model` object, a `.mod`/`.ctl` path, a run +/// directory, or an existing `.ext` path. For `.ext` paths the file is used +/// directly; everything else routes through `load_model_from_input` so +/// `$EST FILE=` overrides are honored. +fn resolve_ext_input(input: &Robj) -> Result { + if let Some(s) = input.as_str() { + let path = Path::new(s); + if path.extension() == Some(OsStr::new("ext")) { + return find_output_file(path, "ext"); + } + } + let loc = load_model_from_input(input)?; + let resolved = resolve_ext_path(&loc.model, &loc.run_dir, &loc.stem); + if !resolved.exists() { + return Err(extendr_err!("Ext file not found: {}", resolved.display())); + } + Ok(resolved) +} + /// Gets all final estimates from an ext file or vector of ext files /// /// @param paths path to directory containing ext files (including subdirectories), single ext file, or vector of ext file paths @@ -317,10 +338,15 @@ pub fn get_final_estimates( return Err(extendr_err!("No tables found in ext file")); } - // Get parameter names from first table (all should be the same) - let param_names = if let Some((_, first_tables)) = results.first() { + // Get parameter names from first table (all should be the same), + // sanitized for R syntactic use. + let param_names: Vec = if let Some((_, first_tables)) = results.first() { if let Some(first_table) = first_tables.first() { - first_table.parameters.clone() + first_table + .parameters + .iter() + .map(|n| to_syntactic_name(n)) + .collect() } else { return Err(extendr_err!("No tables found in first ext file")); } diff --git a/src/rust/nonmem/src/output_files/grd.rs b/src/rust/nonmem/src/output_files/grd.rs index 9d3b0db3..37ab158a 100644 --- a/src/rust/nonmem/src/output_files/grd.rs +++ b/src/rust/nonmem/src/output_files/grd.rs @@ -4,9 +4,21 @@ use extendr_api::prelude::*; //pharos nonmem crate use nonmem::{estimation::EstimationMethod, output_files::grd::GrdReader}; -use crate::utils::{find_output_file, get_comment_type, path_from_robj, try_parse_model}; +use crate::utils::{ + find_output_file, get_comment_type, path_from_robj, to_syntactic_name, try_parse_model, +}; use hyperion_core::{ResultExt, extendr_err}; +/// Turn a pharos-produced gradient column name like `"GRD(IIV (CL))"` into a +/// syntactic R column name like `"IIV_CL"`. +fn sanitize_grd_name(raw: &str) -> String { + let inner = raw + .strip_prefix("GRD(") + .and_then(|s| s.strip_suffix(')')) + .unwrap_or(raw); + to_syntactic_name(inner) +} + fn create_grd_reader(only_method: Option<&str>, only_last: Option) -> Result { let mut reader = GrdReader::default(); @@ -58,13 +70,13 @@ pub fn get_gradients( let search_path = path_from_robj(&path, false)?; let grd_path = find_output_file(&search_path, "grd")?; - let mut model = try_parse_model(search_path.to_string_lossy().as_ref()); + let model = try_parse_model(search_path.to_string_lossy().as_ref()); // Load config and extract comment type let comment_type = get_comment_type(); let tables = grd_reader - .parse_file(grd_path, model.as_mut(), comment_type) + .parse_file(grd_path, model.as_ref(), comment_type) .map_to_extendr_err("")?; if tables.is_empty() { @@ -72,7 +84,12 @@ pub fn get_gradients( } // Get gradient parameter names from the first table (skip ITERATION column) - let gradient_names: Vec = tables[0].parameters.iter().skip(1).cloned().collect(); + let gradient_names: Vec = tables[0] + .parameters + .iter() + .skip(1) + .map(|n| sanitize_grd_name(n)) + .collect(); let flat_data: Vec<(i32, String, Vec)> = tables .into_iter() diff --git a/src/rust/nonmem/src/output_files/transforms.rs b/src/rust/nonmem/src/output_files/transforms.rs index bafce729..0570de52 100644 --- a/src/rust/nonmem/src/output_files/transforms.rs +++ b/src/rust/nonmem/src/output_files/transforms.rs @@ -4,8 +4,8 @@ use extendr_api::scalar::Rfloat; use std::str::FromStr; +use nmparser::Transform; use nonmem::output_files::ext::ParameterType; -use nonmem::transforms::Transform; use hyperion_core::extendr_err; diff --git a/src/rust/nonmem/src/utils.rs b/src/rust/nonmem/src/utils.rs index fca11cea..56b180d1 100644 --- a/src/rust/nonmem/src/utils.rs +++ b/src/rust/nonmem/src/utils.rs @@ -1,14 +1,15 @@ use extendr_api::Result; +use extendr_api::deserializer::from_robj; use extendr_api::prelude::*; use extendr_api::serializer::to_robj; use fs_err as fs; -use std::path::Component; use std::path::{Path, PathBuf}; // pharos config and nonmem crate -use config::{CONFIG_FILENAME, CommentType, Config, NonmemConfig}; +use config::{CONFIG_FILENAME, CommentType, Config, NonmemConfig, to_root_relative}; use nonmem::Model; +use nonmem::output_files::resolve_estimation_files; // hyperion core use hyperion_core::{OptionExt, ResultExt, extendr_err, find_config_dir}; @@ -98,6 +99,116 @@ pub fn find_output_file(input_path: impl AsRef, extension: &str) -> Result } } +/// Parsed model bundled with its on-disk locations, returned by +/// [`load_model_from_input`]. +pub struct ModelLocation { + pub model: Model, + /// The `.mod`/`.ctl` file on disk that was parsed. + pub source: PathBuf, + /// The run output directory (containing `.lst`, `.ext`, etc.). + pub run_dir: PathBuf, + /// The model's stem (e.g. `"run001"`). + pub stem: String, +} + +/// Resolve a polymorphic input to a parsed Model plus its on-disk locations. +/// +/// Accepted inputs: +/// - `hyperion_nonmem_model` Robj (uses its `model_source` attribute). +/// - String path to a `.mod` or `.ctl` file. +/// - String path to a run directory, or any file inside a run directory +/// (e.g. `.ext`, `.lst`, `_metadata.json`). The model copy in the run +/// directory is used as the source. +pub fn load_model_from_input(input: &Robj) -> Result { + if input.inherits("hyperion_nonmem_model") { + let model: Model = from_robj(input)?; + let source = path_from_robj(input, false)?; + return assemble_top_level(model, source); + } + if let Some(s) = input.as_str() { + let path = Path::new(s); + // Direct .mod or .ctl file input. + if let Some(ext) = path.extension().and_then(|e| e.to_str()) + && (ext == "mod" || ext == "ctl") + && path.exists() + { + let model = parse_model_from_disk(path)?; + return assemble_top_level(model, path.to_path_buf()); + } + // Anything else (directory, _metadata.json, .lst, .ext, etc.): locate + // the in-run-dir model copy. `find_output_file` handles directories + // and files adjacent to the run dir (like `_metadata.json`); for files + // that live *inside* the run dir (`.lst`, `.ext`), it would compute a + // doubly-nested wrong path, so fall back to using the file's parent + // as the run dir. + let source = find_output_file(path, "mod") + .or_else(|_| find_output_file(path, "ctl")) + .or_else(|_| { + let parent = path + .parent() + .ok_or_extendr_err("Could not determine parent directory")?; + find_output_file(parent, "mod").or_else(|_| find_output_file(parent, "ctl")) + })?; + let stem = source + .file_stem() + .ok_or_extendr_err("Could not determine model file stem")? + .to_string_lossy() + .to_string(); + let run_dir = source + .parent() + .ok_or_extendr_err("Could not determine run directory")? + .to_path_buf(); + let model = parse_model_from_disk(&source)?; + return Ok(ModelLocation { + model, + source, + run_dir, + stem, + }); + } + Err(extendr_err!( + "Expected a hyperion_nonmem_model object or a path to a model file/directory" + )) +} + +/// Assemble a `ModelLocation` for a source path adjacent to its run dir +/// (the conventional `parent/stem.mod` + `parent/stem/` layout). +fn assemble_top_level(model: Model, source: PathBuf) -> Result { + let stem = source + .file_stem() + .ok_or_extendr_err("Could not determine model file stem")? + .to_string_lossy() + .to_string(); + let run_dir = source + .parent() + .ok_or_extendr_err("Could not determine model parent directory")? + .join(&stem); + Ok(ModelLocation { + model, + source, + run_dir, + stem, + }) +} + +fn parse_model_from_disk(path: &Path) -> Result { + let content = fs::read_to_string(path).map_to_extendr_err("Failed to read model file")?; + Model::parse(&content).map_err(|_| extendr_err!("Failed to parse model: {}", path.display())) +} + +/// Resolve the final `.ext` file path for a model, honoring `$EST FILE=`. +/// +/// Returns the `.ext` path that the *last* `$EST` writes to, after applying +/// NONMEM's inheritance rule (an `$EST` without `FILE=` continues writing to +/// the previous `$EST`'s file). Falls back to `{stem}.ext` in `run_dir` when +/// the model has no `$EST FILE=` overrides. +pub fn resolve_ext_path(model: &Model, run_dir: &Path, stem: &str) -> PathBuf { + let default = run_dir.join(format!("{stem}.ext")); + resolve_estimation_files(model, run_dir, &default) + .pop() + .unwrap_or(default) +} + /// 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 { @@ -148,15 +259,26 @@ pub fn validate_model_path(input_path: impl AsRef) -> Result { Err(extendr_err!("File not found: {}", path.display())) } -/// Convert an absolute path to be relative to the pharos config directory. +/// Convert a path to a project-relative identifier (forward-slash form). +/// +/// Delegates the actual prefix-stripping to pharos's `to_root_relative` for +/// consistency with how the rest of pharos generates project-relative keys. +/// We can't call pharos's `to_config_relative` directly because it uses +/// pharos's own `find_config_dir` and so wouldn't honor the +/// `hyperion.config_dir` R option override. Canonicalizes both sides +/// here so relative inputs (resolved against CWD) line up with the +/// canonical config root before stripping. +/// /// 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")?; if let Some(dir) = config_dir { - let rel = make_relative_path(&dir, path); - return Ok(rel.to_string_lossy().to_string()); + let canonical_path = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); + let canonical_dir = fs::canonicalize(&dir).unwrap_or(dir); + return to_root_relative(&canonical_path, &canonical_dir) + .map_to_extendr_err("Failed to make path config-relative"); } Ok(path.to_string_lossy().to_string()) @@ -209,31 +331,6 @@ pub fn path_from_robj(input: &Robj, validate_model: bool) -> Result { } } -fn make_relative_path(base: &Path, target: &Path) -> PathBuf { - let base_components: Vec> = base.components().collect(); - let target_components: Vec> = target.components().collect(); - - if base_components.first() != target_components.first() { - return target.to_path_buf(); - } - - let mut idx = 0; - let max = base_components.len().min(target_components.len()); - while idx < max && base_components[idx] == target_components[idx] { - idx += 1; - } - - let mut rel = PathBuf::new(); - for _ in idx..base_components.len() { - rel.push(".."); - } - for comp in target_components.iter().skip(idx) { - rel.push(comp.as_os_str()); - } - - rel -} - /// Gives Some(Model) if model path is found pub fn try_parse_model(path: &str) -> Option { let path_buf = std::path::Path::new(path); @@ -257,6 +354,20 @@ pub fn try_parse_model(path: &str) -> Option { /// Gets the comment type from pharos.toml configuration /// +/// Map an arbitrary string to an R-syntactic column name by replacing any +/// non-alphanumeric character with `_`, collapsing runs of `_`, and trimming +/// leading/trailing `_`. E.g. `"SIGMA(1,1)"` -> `"SIGMA_1_1"`. +pub(crate) fn to_syntactic_name(s: &str) -> String { + let mut out: String = s + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect(); + while out.contains("__") { + out = out.replace("__", "_"); + } + out.trim_matches('_').to_string() +} + /// @return Option from pharos config, None if not found or config doesn't exist pub fn get_comment_type() -> Option { find_config_dir() diff --git a/src/rust/nonmem/test_data/run001-running/pharos_end.json b/src/rust/nonmem/test_data/run001-running/pharos_end.json deleted file mode 100644 index c33c1a79..00000000 --- a/src/rust/nonmem/test_data/run001-running/pharos_end.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "start": "2025-09-30T15:10:00+00:00", - "end": "2025-09-30T15:10:20+00:00", - "runtime_ms": 20203, - "files_copied": [], - "output_files_rewrites": { - "run001.tab": "run001.tab", - "run001par.tab": "run001par.tab" - }, - "output_files_hashes": [ - { - "filename": "run001.ext", - "hashes": { - "blake3": "3d02a971fecf60ef371a73cf03d545d628a7387b775b7d2bd896a2a9316b91e2" - } - }, - { - "filename": "run001.grd", - "hashes": { - "blake3": "eaba2933d12232f11a69f42a764640a368b6749dd63ed7e8be40209d47018406" - } - }, - { - "filename": "run001.lst", - "hashes": { - "blake3": "03f24ac8b78209d9231c1e4459cb665c7fd6bc1ee667b2fb20e98a44e71c5a94" - } - }, - { - "filename": "run001.shk", - "hashes": { - "blake3": "9370bd17dbc898ef4f4c8ad77a0a60daddf5bf024e87f70f6622d893f807eed4" - } - }, - { - "filename": "run001.tab", - "hashes": { - "blake3": "c0cd30f4ae77dc53f5cf2695d578a5a341aa175a065083c075496bfd358a05d2" - } - }, - { - "filename": "run001par.tab", - "hashes": { - "blake3": "50df98a5b9be252d35ceeec6ad326267c1b18aeec68e735a9fcd0d483b2ff205" - } - } - ] -} \ No newline at end of file diff --git a/src/rust/nonmem/test_data/run001-running/run001.lst b/src/rust/nonmem/test_data/run001-running/run001.lst index 67ced16f..b267fdc5 100644 --- a/src/rust/nonmem/test_data/run001-running/run001.lst +++ b/src/rust/nonmem/test_data/run001-running/run001.lst @@ -415,6 +415,3 @@ Days until program expires : 139 + 0.00E+00 1.00E-01 Elapsed finaloutput time in seconds: 0.05 - #CPUT: Total CPU Time in Seconds, 0.443 -Stop Time: -Tue Sep 30 15:10:20 UTC 2025 diff --git a/tests/pharos.toml b/tests/pharos.toml deleted file mode 100644 index f41f9d85..00000000 --- a/tests/pharos.toml +++ /dev/null @@ -1,30 +0,0 @@ -[nonmem] -clean_level = 1 -default_version = "nm760" -files_to_copy = [] - -[nonmem.options] -prsame = false -prcompile = false -prdefault = false -tprdefault = false -background = false -nobuild = false -maxlim = 2 - -[nonmem.versions] -nm760 = "/opt/nonmem/nm760" - -[nonmem.parallel] -mpiexec_path = "/opt/homebrew/bin/mpiexec" -enabled = false -num_cpus = 4 -timeout = 2147483647 - -[nonmem.comments] -type = "type1" -error_on_invalid = false - -[nonmem.summary] -high_correlation_threshold = 0.95 -high_condition_threshold = 1000 diff --git a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run001.html b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run001.html index d79b89e8..a4f91bf0 100644 --- a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run001.html +++ b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run001.html @@ -22,7 +22,7 @@ extdata/models/onecmt/run001/run001.lst default default - default + extdata/models/onecmt/run001/run001.lst default @@ -30,7 +30,7 @@ extdata/models/onecmt/run001/run001.lst default default - default + extdata/models/onecmt/run001/run001.lst default @@ -38,7 +38,7 @@ extdata/models/onecmt/run001/run001.lst default default - default + extdata/models/onecmt/run001/run001.lst default @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -63,6 +64,7 @@ OMEGA(1,1) extdata/models/onecmt/run001/run001.lst + extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst @@ -71,6 +73,7 @@ OMEGA(2,2) extdata/models/onecmt/run001/run001.lst + extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst @@ -79,6 +82,7 @@ OMEGA(3,3) extdata/models/onecmt/run001/run001.lst + extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst @@ -105,7 +109,7 @@ SIGMA(1,1) - extdata/models/onecmt/run001/run001.lst + default default default default @@ -113,7 +117,7 @@ SIGMA(2,2) - extdata/models/onecmt/run001/run001.lst + default default default default diff --git a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run002.html b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run002.html index 836d6447..65b91765 100644 --- a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run002.html +++ b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run002.html @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -63,6 +64,7 @@ OMEGA(1,1) extdata/models/onecmt/run002/run002.lst + extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst @@ -71,6 +73,7 @@ OMEGA(2,2) extdata/models/onecmt/run002/run002.lst + extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst @@ -79,6 +82,7 @@ OMEGA(3,3) extdata/models/onecmt/run002/run002.lst + extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst @@ -105,7 +109,7 @@ SIGMA(1,1) - extdata/models/onecmt/run002/run002.lst + default default default default @@ -113,7 +117,7 @@ SIGMA(2,2) - extdata/models/onecmt/run002/run002.lst + default default default default diff --git a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003.html b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003.html index 0052256f..5e483292 100644 --- a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003.html +++ b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003.html @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -63,6 +64,7 @@ OMEGA(1,1) extdata/models/onecmt/run003/run003.lst + extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst @@ -71,6 +73,7 @@ OMEGA(2,1) extdata/models/onecmt/run003/run003.lst + extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst @@ -79,6 +82,7 @@ OMEGA(2,2) extdata/models/onecmt/run003/run003.lst + extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst @@ -87,6 +91,7 @@ OMEGA(3,3) extdata/models/onecmt/run003/run003.lst + extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst @@ -113,7 +118,7 @@ SIGMA(1,1) - extdata/models/onecmt/run003/run003.lst + default default default default @@ -121,7 +126,7 @@ SIGMA(2,2) - extdata/models/onecmt/run003/run003.lst + default default default default diff --git a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003b1.html b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003b1.html index 21a6cfaa..5a7add14 100644 --- a/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003b1.html +++ b/tests/testthat/_snaps/hyperion-audit-knit/audit-knit-run003b1.html @@ -27,7 +27,7 @@ THETA2 - extdata/models/onecmt/run003b1/run003b1.lst + default default default default @@ -61,6 +61,7 @@ parameter name + raw_name display description parameterization @@ -71,6 +72,7 @@ OMEGA(1,1) extdata/models/onecmt/run003b1/run003b1.lst + extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst @@ -79,6 +81,7 @@ OMEGA(2,1) extdata/models/onecmt/run003b1/run003b1.lst + extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst @@ -87,6 +90,7 @@ OMEGA(2,2) extdata/models/onecmt/run003b1/run003b1.lst + extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst @@ -95,6 +99,7 @@ OMEGA(3,3) extdata/models/onecmt/run003b1/run003b1.lst + extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst @@ -121,7 +126,7 @@ SIGMA(1,1) - extdata/models/onecmt/run003b1/run003b1.lst + default default default default @@ -129,7 +134,7 @@ SIGMA(2,2) - extdata/models/onecmt/run003b1/run003b1.lst + default default default default diff --git a/tests/testthat/_snaps/hyperion-audit-print.md b/tests/testthat/_snaps/hyperion-audit-print.md index e9e6078c..e0fd4e9a 100644 --- a/tests/testthat/_snaps/hyperion-audit-print.md +++ b/tests/testthat/_snaps/hyperion-audit-print.md @@ -14,32 +14,32 @@ Output - parameter name display description unit parameterization - ───────── ─────────────────────────────────────── ─────── ─────────── ─────── ──────────────── - THETA1 extdata/models/onecmt/run001/run001.lst default default default default - THETA2 extdata/models/onecmt/run001/run001.lst default default default default - THETA3 extdata/models/onecmt/run001/run001.lst default default default default + parameter name display description unit parameterization + ───────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ──────────────── + THETA1 extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst default + THETA2 extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst default + THETA3 extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst default Message -- Omega Sources -- Output - parameter name display description parameterization associated_theta - ────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── - OMEGA(1,1) extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst - OMEGA(2,2) extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst - OMEGA(3,3) extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst + parameter name raw_name display description parameterization associated_theta + ────────── ─────────────────────────────────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── + OMEGA(1,1) extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst + OMEGA(2,2) extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst + OMEGA(3,3) extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst default default extdata/models/onecmt/run001/run001.lst extdata/models/onecmt/run001/run001.lst Message -- Sigma Sources -- Output - parameter name display description unit parameterization - ────────── ─────────────────────────────────────── ─────── ─────────── ─────── ──────────────── - SIGMA(1,1) extdata/models/onecmt/run001/run001.lst default default default default - SIGMA(2,2) extdata/models/onecmt/run001/run001.lst default default default default + parameter name display description unit parameterization + ────────── ─────── ─────── ─────────── ─────── ──────────────── + SIGMA(1,1) default default default default default + SIGMA(2,2) default default default default default --- @@ -68,21 +68,21 @@ Output - parameter name display description parameterization associated_theta - ────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── - OMEGA(1,1) extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst - OMEGA(2,2) extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst - OMEGA(3,3) extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst + parameter name raw_name display description parameterization associated_theta + ────────── ─────────────────────────────────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── + OMEGA(1,1) extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst + OMEGA(2,2) extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst + OMEGA(3,3) extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst default default extdata/models/onecmt/run002/run002.lst extdata/models/onecmt/run002/run002.lst Message -- Sigma Sources -- Output - parameter name display description unit parameterization - ────────── ─────────────────────────────────────── ─────── ─────────── ─────── ──────────────── - SIGMA(1,1) extdata/models/onecmt/run002/run002.lst default default default default - SIGMA(2,2) extdata/models/onecmt/run002/run002.lst default default default default + parameter name display description unit parameterization + ────────── ─────── ─────── ─────────── ─────── ──────────────── + SIGMA(1,1) default default default default default + SIGMA(2,2) default default default default default --- @@ -111,22 +111,22 @@ Output - parameter name display description parameterization associated_theta - ────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── - OMEGA(1,1) extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst - OMEGA(2,1) extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst - OMEGA(2,2) extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst - OMEGA(3,3) extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst + parameter name raw_name display description parameterization associated_theta + ────────── ─────────────────────────────────────── ─────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────── ─────────────────────────────────────── + OMEGA(1,1) extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst + OMEGA(2,1) extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst + OMEGA(2,2) extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst + OMEGA(3,3) extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst default default extdata/models/onecmt/run003/run003.lst extdata/models/onecmt/run003/run003.lst Message -- Sigma Sources -- Output - parameter name display description unit parameterization - ────────── ─────────────────────────────────────── ─────── ─────────── ─────── ──────────────── - SIGMA(1,1) extdata/models/onecmt/run003/run003.lst default default default default - SIGMA(2,2) extdata/models/onecmt/run003/run003.lst default default default default + parameter name display description unit parameterization + ────────── ─────── ─────── ─────────── ─────── ──────────────── + SIGMA(1,1) default default default default default + SIGMA(2,2) default default default default default --- @@ -147,7 +147,7 @@ parameter name display description unit parameterization ───────── ─────────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────────── ──────────────── THETA1 extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst default - THETA2 extdata/models/onecmt/run003b1/run003b1.lst default default default default + THETA2 default default default default default THETA3 extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst default THETA4 extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst default @@ -156,20 +156,20 @@ Output - parameter name display description parameterization associated_theta - ────────── ─────────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────────── ─────────────────────────────────────────── - OMEGA(1,1) extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst - OMEGA(2,1) extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst - OMEGA(2,2) extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst - OMEGA(3,3) extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst + parameter name raw_name display description parameterization associated_theta + ────────── ─────────────────────────────────────────── ─────────────────────────────────────────── ─────── ─────────── ─────────────────────────────────────────── ─────────────────────────────────────────── + OMEGA(1,1) extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst + OMEGA(2,1) extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst + OMEGA(2,2) extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst + OMEGA(3,3) extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst default default extdata/models/onecmt/run003b1/run003b1.lst extdata/models/onecmt/run003b1/run003b1.lst Message -- Sigma Sources -- Output - parameter name display description unit parameterization - ────────── ─────────────────────────────────────────── ─────── ─────────── ─────── ──────────────── - SIGMA(1,1) extdata/models/onecmt/run003b1/run003b1.lst default default default default - SIGMA(2,2) extdata/models/onecmt/run003b1/run003b1.lst default default default default + parameter name display description unit parameterization + ────────── ─────── ─────── ─────────── ─────── ──────────────── + SIGMA(1,1) default default default default default + SIGMA(2,2) default default default default default diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-1001.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-1001.html index 960962a5..7e6574d7 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-1001.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-1001.html @@ -5,11 +5,12 @@ Run Status: Not Run - Dataset: ../../../../data/derived/PK_Oral_Ex1.csv Ignore: @ +Aliased Columns: ATFD → TIME, ODV → DV + Theta Parameters @@ -74,13 +75,13 @@ OMEGA(1,1) 0.1 No - OM1 CL :EXP + OM1 CL/F :EXP OMEGA(2,2) 0.1 No - OM2 VC :EXP + OM2 VC/F :EXP OMEGA(3,3) diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-everything.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-everything.html index a0733bf4..72501bca 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-everything.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-everything.html @@ -5,16 +5,15 @@ Run Status: Not Run +Dataset: ../path with spaces/data.csv -Dataset: ../data.csv - -Ignore: #, DVID.EQ.3, ID.EQ.3.14 +Ignore: #, DVID.EQ.3, ID.EQ.3.14, DVID.EQ.3, AGE.GE.18, AGE.GT.3, AGE.LT.100, AGE.LE.65, TYPE.NE.0, TYPE.EQ.1, TYPE.EQN.1, TYPE.NEN.2, TYPE.EQ.1 Records: 200 Dropped Columns: DATE -Aliased Columns: AMT → DOSE +Aliased Columns: DOSE → AMT @@ -50,6 +49,102 @@ THETA3 + 0.5 + -Inf + 10 + No + THETA with -INF lower bound + + + THETA4 + 5 + 0 + Inf + No + THETA with INF upper bound + + + THETA5 + 0.1 + 0 + NA + No + Three identical THETAs + + + THETA6 + 0.1 + 0 + NA + No + Three identical THETAs + + + THETA7 + 0.1 + 0 + NA + No + Three identical THETAs + + + THETA8 + 1.5 + 0 + 10 + No + Named THETA + + + THETA9 + 0.5 + 0 + NA + No + NAMES syntax + + + THETA10 + 10 + 0 + NA + No + NAMES syntax + + + THETA11 + 2 + 0 + NA + No + NAMES syntax + + + THETA12 + 1.1 + 1 + NA + No + Three identical THETAs with NAMES + + + THETA13 + 1.1 + 1 + NA + No + Three identical THETAs with NAMES + + + THETA14 + 1.1 + 1 + NA + No + Three identical THETAs with NAMES + + + THETA15 2.3 NA NA @@ -57,7 +152,7 @@ THETA(3) - THETA4 + THETA16 0.8 NA NA @@ -65,7 +160,7 @@ THETA(4) and THETA(5) - THETA5 + THETA17 0.25 NA NA @@ -73,7 +168,7 @@ THETA(4) and THETA(5) - THETA6 + THETA18 2.3 1 NA @@ -81,7 +176,7 @@ THETA(6) - THETA7 + THETA19 0.75 NA NA @@ -100,8 +195,6 @@ Parameter Initial - Lower - Upper Fixed Parametrization Comment @@ -111,8 +204,6 @@ OMEGA(1,1) 0.04 - NA - NA No ETA(1) - CL (diagonal) @@ -120,8 +211,6 @@ OMEGA(2,2) 0.17 - NA - NA No @@ -129,8 +218,6 @@ OMEGA(3,3) 0.2 - NA - NA No Correlation ETA(2) - V (SD) @@ -138,8 +225,6 @@ OMEGA(4,3) 0.3 - NA - NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) @@ -147,8 +232,6 @@ OMEGA(4,4) 0.15 - NA - NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) @@ -156,8 +239,6 @@ OMEGA(5,5) 0.2 - NA - NA No Correlation ETA(2) - V (SD) @@ -165,8 +246,6 @@ OMEGA(6,5) 0.3 - NA - NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) @@ -174,20 +253,324 @@ OMEGA(6,6) 0.15 - NA - NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) OMEGA(7,7) - 0.1 + 0.01121 + Yes + + + + + OMEGA(8,7) 0 - 1 Yes - ETA(6) - fixed diagonal + + + + OMEGA(8,8) + 0.3387 + Yes + + + + + OMEGA(9,9) + 0.1 + No + + + + + OMEGA(10,9) + 0.01 + No + + + + + OMEGA(10,10) + 0.1 + No + + + + + OMEGA(11,9) + 0.01 + No + + + + + OMEGA(11,10) + 0.01 + No + + + + + OMEGA(11,11) + 0.1 + No + + + + + OMEGA(12,9) + 0.01 + No + + + + + OMEGA(12,10) + 0.01 + No + + + + + OMEGA(12,11) + 0.01 + No + + + + + OMEGA(12,12) + 0.1 + No + + + + + OMEGA(13,13) + 0.4 + No + + Label=Value syntax for diagonal + + + OMEGA(14,14) + 0.3 + No + + + + + OMEGA(15,14) + 0.01 + No + + Label=Value syntax in block + + + OMEGA(15,15) + 0.35 + No + + Label=Value syntax in block + + + OMEGA(16,16) + 0.03 + No + + + + + OMEGA(17,16) + 0.01 + No + + + + + OMEGA(17,17) + 0.03 + No + + + + + OMEGA(18,16) + 0.01 + No + + + + + OMEGA(18,17) + 0.01 + No + + + + + OMEGA(18,18) + 0.03 + No + + + + + OMEGA(19,16) + 0.01 + No + + + + + OMEGA(19,17) + 0.01 + No + + + + + OMEGA(19,18) + 0.01 + No + + + + + OMEGA(19,19) + 0.03 + No + + + + + OMEGA(20,20) + 0.2 + No + Correlation + + + + OMEGA(21,20) + 0.3 + No + Correlation + + + + OMEGA(21,21) + 0.15 + No + Correlation + + + + OMEGA(22,20) + 0.1 + No + Correlation + + + + OMEGA(22,21) + 0.05 + No + Correlation + + + + OMEGA(22,22) + 0.3 + No + Correlation + + + + OMEGA(23,23) + 0.2 + No + Correlation + + + + OMEGA(24,23) + 0.3 + No + Correlation + + + + OMEGA(24,24) + 0.15 + No + Correlation + + + + OMEGA(25,23) + 0.1 + No + Correlation + + + + OMEGA(25,24) + 0.05 + No + Correlation + + + + OMEGA(25,25) + 0.3 + No + Correlation + + + + OMEGA(26,26) + 6 + Yes + + + + + OMEGA(27,26) + 0.005 + Yes + + + + + OMEGA(27,27) + 0.3 + Yes + + + + + OMEGA(28,26) + 0.001 + Yes + + + + + OMEGA(28,27) + 0.002 + Yes + + + + + OMEGA(28,28) + 0.1 + Yes + + @@ -224,6 +607,36 @@ No Prop-Add covariance, Additive error variance + + SIGMA(3,3) + 1 + Yes + + + + SIGMA(4,4) + 0.036 + No + + + + SIGMA(5,5) + 0.04 + No + Label=Value syntax for SIGMA + + + SIGMA(6,6) + 0.01 + No + diagonal SIGMA + + + SIGMA(7,7) + 0.02 + No + diagonal SIGMA + diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-example1.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-example1.html index a6b31d40..91f2f7cf 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-example1.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-example1.html @@ -5,12 +5,11 @@ Run Status: Not Run - Dataset: example1.csv Ignore: C -Aliased Columns: CONC → DV, DOSE → AMT +Aliased Columns: DV → CONC, AMT → DOSE diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-iiv-cov.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-iiv-cov.html index b89ebe7b..a7132b30 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-iiv-cov.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-iiv-cov.html @@ -5,11 +5,12 @@ Run Status: Not Run - Dataset: ../../data/derived/PK_Oral_Ex1.csv Ignore: @ +Aliased Columns: ATFD → TIME, ODV → DV + Theta Parameters diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-iov.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-iov.html index 15c7430c..b72111b4 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-iov.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-iov.html @@ -5,7 +5,6 @@ Run Status: Not Run - Dataset: test.csv Ignore: @ diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-multiline_table.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-multiline_table.html index ec3052e5..adeefebc 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-multiline_table.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-multiline_table.html @@ -5,12 +5,11 @@ Run Status: Not Run - Dataset: ../data.csv Dropped Columns: DATE -Aliased Columns: AMT → DOSE +Aliased Columns: DOSE → AMT diff --git a/tests/testthat/_snaps/hyperion-model-knit/model-knit-nmexample.html b/tests/testthat/_snaps/hyperion-model-knit/model-knit-nmexample.html index 1615d741..d5fe8aa0 100644 --- a/tests/testthat/_snaps/hyperion-model-knit/model-knit-nmexample.html +++ b/tests/testthat/_snaps/hyperion-model-knit/model-knit-nmexample.html @@ -5,12 +5,11 @@ Run Status: Not Run - Dataset: example1.csv Ignore: C -Aliased Columns: CONC → DV, DOSE → AMT +Aliased Columns: DV → CONC, AMT → DOSE diff --git a/tests/testthat/_snaps/hyperion-model-print.md b/tests/testthat/_snaps/hyperion-model-print.md index b8f5e1d5..d8446642 100644 --- a/tests/testthat/_snaps/hyperion-model-print.md +++ b/tests/testthat/_snaps/hyperion-model-print.md @@ -10,6 +10,7 @@ Run Status: Not Run Dataset: ../../../../data/derived/PK_Oral_Ex1.csv Ignore: @ + Aliased Columns: ATFD→TIME, ODV→DV Output Message @@ -30,11 +31,11 @@ Output - Parameter Initial Fixed Comment - ────────── ─────── ───── ─────────── - OMEGA(1,1) 0.1 No OM1 CL :EXP - OMEGA(2,2) 0.1 No OM2 VC :EXP - OMEGA(3,3) 0.1 No OM3 KA :EXP + Parameter Initial Fixed Comment + ────────── ─────── ───── ───────────── + OMEGA(1,1) 0.1 No OM1 CL/F :EXP + OMEGA(2,2) 0.1 No OM2 VC/F :EXP + OMEGA(3,3) 0.1 No OM3 KA :EXP Message -- Sigma Parameters -- @@ -56,11 +57,12 @@ -- NONMEM Model: everything ---------------------------------------------------- Problem: Some header #2 Run Status: Not Run - Dataset: ..\data.csv - Ignore: #, DVID.EQ.3, ID.EQ.3.14 + Dataset: ..\path with spaces\data.csv + Ignore: #, DVID.EQ.3, ID.EQ.3.14, DVID.EQ.3, AGE.GE.18, AGE.GT.3, AGE.LT.100, + AGE.LE.65, TYPE.NE.0, TYPE.EQ.1, TYPE.EQN.1, TYPE.NEN.2, TYPE.EQ.1 Records: 200 Dropped Columns: DATE - Aliased Columns: AMT→DOSE + Aliased Columns: DOSE→AMT Output Message @@ -69,32 +71,88 @@ Output - Parameter Initial Lower Upper Fixed Comment - ───────── ─────── ───── ───── ───── ───────────────────── - THETA1 1.5 NA NA No THETA(1) and THETA(2) - THETA2 0.5 0 2 No THETA(1) and THETA(2) - THETA3 2.3 NA NA Yes THETA(3) - THETA4 0.8 NA NA No THETA(4) and THETA(5) - THETA5 0.25 NA NA No THETA(4) and THETA(5) - THETA6 2.3 1 NA Yes THETA(6) - THETA7 0.75 NA NA Yes THETA(7) + Parameter Initial Lower Upper Fixed Comment + ───────── ─────── ───── ───── ───── ───────────────────────────────── + THETA1 1.5 NA NA No THETA(1) and THETA(2) + THETA2 0.5 0 2 No THETA(1) and THETA(2) + THETA3 0.5 -Inf 10 No THETA with -INF lower bound + THETA4 5 0 Inf No THETA with INF upper bound + THETA5 0.1 0 NA No Three identical THETAs + THETA6 0.1 0 NA No Three identical THETAs + THETA7 0.1 0 NA No Three identical THETAs + THETA8 1.5 0 10 No Named THETA + THETA9 0.5 0 NA No NAMES syntax + THETA10 10 0 NA No NAMES syntax + THETA11 2 0 NA No NAMES syntax + THETA12 1.1 1 NA No Three identical THETAs with NAMES + THETA13 1.1 1 NA No Three identical THETAs with NAMES + THETA14 1.1 1 NA No Three identical THETAs with NAMES + THETA15 2.3 NA NA Yes THETA(3) + THETA16 0.8 NA NA No THETA(4) and THETA(5) + THETA17 0.25 NA NA No THETA(4) and THETA(5) + THETA18 2.3 1 NA Yes THETA(6) + THETA19 0.75 NA NA Yes THETA(7) Message -- Omega Parameters -- Output - Parameter Initial Lower Upper Fixed Parametrization Comment - ────────── ─────── ───── ───── ───── ─────────────── ─────────────────────────────────────────── - OMEGA(1,1) 0.04 NA NA No ETA(1) - CL (diagonal) - OMEGA(2,2) 0.17 NA NA No - OMEGA(3,3) 0.2 NA NA No Correlation ETA(2) - V (SD) - OMEGA(4,3) 0.3 NA NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) - OMEGA(4,4) 0.15 NA NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) - OMEGA(5,5) 0.2 NA NA No Correlation ETA(2) - V (SD) - OMEGA(6,5) 0.3 NA NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) - OMEGA(6,6) 0.15 NA NA No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) - OMEGA(7,7) 0.1 0 1 Yes ETA(6) - fixed diagonal + Parameter Initial Fixed Parametrization Comment + ──────────── ─────── ───── ─────────────── ─────────────────────────────────────────── + OMEGA(1,1) 0.04 No ETA(1) - CL (diagonal) + OMEGA(2,2) 0.17 No + OMEGA(3,3) 0.2 No Correlation ETA(2) - V (SD) + OMEGA(4,3) 0.3 No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) + OMEGA(4,4) 0.15 No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) + OMEGA(5,5) 0.2 No Correlation ETA(2) - V (SD) + OMEGA(6,5) 0.3 No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) + OMEGA(6,6) 0.15 No Correlation ETA(2)-ETA(3) correlation, ETA(3) - KA (SD) + OMEGA(7,7) 0.01121 Yes + OMEGA(8,7) 0 Yes + OMEGA(8,8) 0.3387 Yes + OMEGA(9,9) 0.1 No + OMEGA(10,9) 0.01 No + OMEGA(10,10) 0.1 No + OMEGA(11,9) 0.01 No + OMEGA(11,10) 0.01 No + OMEGA(11,11) 0.1 No + OMEGA(12,9) 0.01 No + OMEGA(12,10) 0.01 No + OMEGA(12,11) 0.01 No + OMEGA(12,12) 0.1 No + OMEGA(13,13) 0.4 No Label=Value syntax for diagonal + OMEGA(14,14) 0.3 No + OMEGA(15,14) 0.01 No Label=Value syntax in block + OMEGA(15,15) 0.35 No Label=Value syntax in block + OMEGA(16,16) 0.03 No + OMEGA(17,16) 0.01 No + OMEGA(17,17) 0.03 No + OMEGA(18,16) 0.01 No + OMEGA(18,17) 0.01 No + OMEGA(18,18) 0.03 No + OMEGA(19,16) 0.01 No + OMEGA(19,17) 0.01 No + OMEGA(19,18) 0.01 No + OMEGA(19,19) 0.03 No + OMEGA(20,20) 0.2 No Correlation + OMEGA(21,20) 0.3 No Correlation + OMEGA(21,21) 0.15 No Correlation + OMEGA(22,20) 0.1 No Correlation + OMEGA(22,21) 0.05 No Correlation + OMEGA(22,22) 0.3 No Correlation + OMEGA(23,23) 0.2 No Correlation + OMEGA(24,23) 0.3 No Correlation + OMEGA(24,24) 0.15 No Correlation + OMEGA(25,23) 0.1 No Correlation + OMEGA(25,24) 0.05 No Correlation + OMEGA(25,25) 0.3 No Correlation + OMEGA(26,26) 6 Yes + OMEGA(27,26) 0.005 Yes + OMEGA(27,27) 0.3 Yes + OMEGA(28,26) 0.001 Yes + OMEGA(28,27) 0.002 Yes + OMEGA(28,28) 0.1 Yes Message -- Sigma Parameters -- @@ -106,6 +164,11 @@ SIGMA(1,1) 0.01 No Proportional error variance SIGMA(2,1) 0.002 No Prop-Add covariance, Additive error variance SIGMA(2,2) 0.25 No Prop-Add covariance, Additive error variance + SIGMA(3,3) 1 Yes + SIGMA(4,4) 0.036 No + SIGMA(5,5) 0.04 No Label=Value syntax for SIGMA + SIGMA(6,6) 0.01 No diagonal SIGMA + SIGMA(7,7) 0.02 No diagonal SIGMA --- @@ -119,7 +182,7 @@ Run Status: Not Run Dataset: example1.csv Ignore: C - Aliased Columns: CONC→DV, DOSE→AMT + Aliased Columns: DV→CONC, AMT→DOSE Output Message @@ -175,6 +238,7 @@ Run Status: Not Run Dataset: ../../data/derived/PK_Oral_Ex1.csv Ignore: @ + Aliased Columns: ATFD→TIME, ODV→DV Output Message @@ -286,7 +350,7 @@ Run Status: Not Run Dataset: ..\data.csv Dropped Columns: DATE - Aliased Columns: AMT→DOSE + Aliased Columns: DOSE→AMT Output Message @@ -315,7 +379,7 @@ Run Status: Not Run Dataset: example1.csv Ignore: C - Aliased Columns: CONC→DV, DOSE→AMT + Aliased Columns: DV→CONC, AMT→DOSE Output Message diff --git a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run001.html b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run001.html index 437ff15a..8824b7da 100644 --- a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run001.html +++ b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run001.html @@ -22,7 +22,7 @@ TVCL NA NA - NA + L/hr NA @@ -30,7 +30,7 @@ TVV NA NA - NA + L NA @@ -38,7 +38,7 @@ TVKA NA NA - NA + 1/hr NA @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -62,6 +63,7 @@ OMEGA(1,1) + OM1 (TVCL) OM1 NA NA @@ -70,6 +72,7 @@ OMEGA(2,2) + OM2 (TVV) OM2 NA NA @@ -78,6 +81,7 @@ OMEGA(3,3) + OM3 (TVKA) OM3 NA NA @@ -105,7 +109,7 @@ SIGMA(1,1) - Proportional + NA NA NA NA @@ -113,7 +117,7 @@ SIGMA(2,2) - Additive + NA NA NA NA diff --git a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run002.html b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run002.html index cfdb4047..8824b7da 100644 --- a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run002.html +++ b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run002.html @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -62,6 +63,7 @@ OMEGA(1,1) + OM1 (TVCL) OM1 NA NA @@ -70,6 +72,7 @@ OMEGA(2,2) + OM2 (TVV) OM2 NA NA @@ -78,6 +81,7 @@ OMEGA(3,3) + OM3 (TVKA) OM3 NA NA @@ -105,7 +109,7 @@ SIGMA(1,1) - SIG1 + NA NA NA NA @@ -113,7 +117,7 @@ SIGMA(2,2) - SIG2 + NA NA NA NA diff --git a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003.html b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003.html index 85f39e42..7afede63 100644 --- a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003.html +++ b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003.html @@ -53,6 +53,7 @@ parameter name + raw_name display description parameterization @@ -62,6 +63,7 @@ OMEGA(1,1) + OM1 (TVCL) OM1 NA NA @@ -70,6 +72,7 @@ OMEGA(2,1) + OM1,2 (TVCL, TVV) OM1,2 NA NA @@ -78,6 +81,7 @@ OMEGA(2,2) + OM2 (TVV) OM2 NA NA @@ -86,6 +90,7 @@ OMEGA(3,3) + OM3 (TVKA) OM3 NA NA @@ -113,7 +118,7 @@ SIGMA(1,1) - SIG1 + NA NA NA NA @@ -121,7 +126,7 @@ SIGMA(2,2) - SIG2 + NA NA NA NA diff --git a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003b1.html b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003b1.html index 78e91bc1..2be3a872 100644 --- a/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003b1.html +++ b/tests/testthat/_snaps/hyperion-parameter-info-knit/parameter-info-knit-run003b1.html @@ -27,7 +27,7 @@ THETA2 - WT-on-CL + NA NA NA NA @@ -61,6 +61,7 @@ parameter name + raw_name display description parameterization @@ -70,6 +71,7 @@ OMEGA(1,1) + OM1 (TVCL) OM1 NA NA @@ -78,6 +80,7 @@ OMEGA(2,1) + OM1,2 (TVCL, TVV) OM1,2 NA NA @@ -86,6 +89,7 @@ OMEGA(2,2) + OM2 (TVV) OM2 NA NA @@ -94,6 +98,7 @@ OMEGA(3,3) + OM3 (TVKA) OM3 NA NA @@ -121,7 +126,7 @@ SIGMA(1,1) - SIG1 + NA NA NA NA @@ -129,7 +134,7 @@ SIGMA(2,2) - SIG2 + NA NA NA NA diff --git a/tests/testthat/_snaps/hyperion-parameter-info-print.md b/tests/testthat/_snaps/hyperion-parameter-info-print.md index 8522d882..30a78db8 100644 --- a/tests/testthat/_snaps/hyperion-parameter-info-print.md +++ b/tests/testthat/_snaps/hyperion-parameter-info-print.md @@ -16,30 +16,30 @@ parameter name display description unit parameterization ───────── ──── ─────── ─────────── ──── ──────────────── - THETA1 TVCL NA NA NA NA - THETA2 TVV NA NA NA NA - THETA3 TVKA NA NA NA NA + THETA1 TVCL NA NA L/hr NA + THETA2 TVV NA NA L NA + THETA3 TVKA NA NA 1/hr NA Message -- Omega Parameters -- Output - parameter name display description parameterization associated_theta - ────────── ──── ─────── ─────────── ──────────────── ──────────────── - OMEGA(1,1) OM1 NA NA LogNormal TVCL - OMEGA(2,2) OM2 NA NA LogNormal TVV - OMEGA(3,3) OM3 NA NA LogNormal TVKA + parameter name raw_name display description parameterization associated_theta + ────────── ────────── ──────── ─────── ─────────── ──────────────── ──────────────── + OMEGA(1,1) OM1 (TVCL) OM1 NA NA LogNormal TVCL + OMEGA(2,2) OM2 (TVV) OM2 NA NA LogNormal TVV + OMEGA(3,3) OM3 (TVKA) OM3 NA NA LogNormal TVKA Message -- Sigma Parameters -- Output - parameter name display description unit parameterization - ────────── ──────────── ─────── ─────────── ──── ──────────────── - SIGMA(1,1) Proportional NA NA NA NA - SIGMA(2,2) Additive NA NA NA NA + parameter name display description unit parameterization + ────────── ──── ─────── ─────────── ──── ──────────────── + SIGMA(1,1) NA NA NA NA NA + SIGMA(2,2) NA NA NA NA NA --- @@ -68,11 +68,11 @@ Output - parameter name display description parameterization associated_theta - ────────── ──── ─────── ─────────── ──────────────── ──────────────── - OMEGA(1,1) OM1 NA NA LogNormal TVCL - OMEGA(2,2) OM2 NA NA LogNormal TVV - OMEGA(3,3) OM3 NA NA LogNormal TVKA + parameter name raw_name display description parameterization associated_theta + ────────── ────────── ──────── ─────── ─────────── ──────────────── ──────────────── + OMEGA(1,1) OM1 (TVCL) OM1 NA NA LogNormal TVCL + OMEGA(2,2) OM2 (TVV) OM2 NA NA LogNormal TVV + OMEGA(3,3) OM3 (TVKA) OM3 NA NA LogNormal TVKA Message -- Sigma Parameters -- @@ -81,8 +81,8 @@ parameter name display description unit parameterization ────────── ──── ─────── ─────────── ──── ──────────────── - SIGMA(1,1) SIG1 NA NA NA NA - SIGMA(2,2) SIG2 NA NA NA NA + SIGMA(1,1) NA NA NA NA NA + SIGMA(2,2) NA NA NA NA NA --- @@ -111,12 +111,12 @@ Output - parameter name display description parameterization associated_theta - ────────── ───── ─────── ─────────── ──────────────── ──────────────── - OMEGA(1,1) OM1 NA NA LogNormal TVCL - OMEGA(2,1) OM1,2 NA NA LogNormal TVCL, TVV - OMEGA(2,2) OM2 NA NA LogNormal TVV - OMEGA(3,3) OM3 NA NA LogNormal TVKA + parameter name raw_name display description parameterization associated_theta + ────────── ───────────────── ──────── ─────── ─────────── ──────────────── ──────────────── + OMEGA(1,1) OM1 (TVCL) OM1 NA NA LogNormal TVCL + OMEGA(2,1) OM1,2 (TVCL, TVV) OM1,2 NA NA LogNormal TVCL, TVV + OMEGA(2,2) OM2 (TVV) OM2 NA NA LogNormal TVV + OMEGA(3,3) OM3 (TVKA) OM3 NA NA LogNormal TVKA Message -- Sigma Parameters -- @@ -125,8 +125,8 @@ parameter name display description unit parameterization ────────── ──── ─────── ─────────── ──── ──────────────── - SIGMA(1,1) SIG1 NA NA NA NA - SIGMA(2,2) SIG2 NA NA NA NA + SIGMA(1,1) NA NA NA NA NA + SIGMA(2,2) NA NA NA NA NA --- @@ -144,24 +144,24 @@ Output - parameter name display description unit parameterization - ───────── ──────── ─────── ─────────── ──── ──────────────── - THETA1 TVCL NA NA L/hr NA - THETA2 WT-on-CL NA NA NA NA - THETA3 TVV NA NA L NA - THETA4 TVKA NA NA 1/hr NA + parameter name display description unit parameterization + ───────── ──── ─────── ─────────── ──── ──────────────── + THETA1 TVCL NA NA L/hr NA + THETA2 NA NA NA NA NA + THETA3 TVV NA NA L NA + THETA4 TVKA NA NA 1/hr NA Message -- Omega Parameters -- Output - parameter name display description parameterization associated_theta - ────────── ───── ─────── ─────────── ──────────────── ──────────────── - OMEGA(1,1) OM1 NA NA LogNormal TVCL - OMEGA(2,1) OM1,2 NA NA LogNormal TVCL, TVV - OMEGA(2,2) OM2 NA NA LogNormal TVV - OMEGA(3,3) OM3 NA NA LogNormal TVKA + parameter name raw_name display description parameterization associated_theta + ────────── ───────────────── ──────── ─────── ─────────── ──────────────── ──────────────── + OMEGA(1,1) OM1 (TVCL) OM1 NA NA LogNormal TVCL + OMEGA(2,1) OM1,2 (TVCL, TVV) OM1,2 NA NA LogNormal TVCL, TVV + OMEGA(2,2) OM2 (TVV) OM2 NA NA LogNormal TVV + OMEGA(3,3) OM3 (TVKA) OM3 NA NA LogNormal TVKA Message -- Sigma Parameters -- @@ -170,8 +170,8 @@ parameter name display description unit parameterization ────────── ──── ─────── ─────────── ──── ──────────────── - SIGMA(1,1) SIG1 NA NA NA NA - SIGMA(2,2) SIG2 NA NA NA NA + SIGMA(1,1) NA NA NA NA NA + SIGMA(2,2) NA NA NA NA NA # hyperion_nonmem_parameter_info works for unrun model input @@ -201,11 +201,11 @@ Output - parameter name display description parameterization associated_theta - ────────── ──── ─────── ─────────── ──────────────── ──────────────── - OMEGA(1,1) OM1 NA NA LogNormal CL/F - OMEGA(2,2) OM2 NA NA LogNormal VC/F - OMEGA(3,3) OM3 NA NA LogNormal KA + parameter name raw_name display description parameterization associated_theta + ────────── ────────── ──────── ─────── ─────────── ──────────────── ──────────────── + OMEGA(1,1) OM1 (CL/F) OM1 NA NA LogNormal CL/F + OMEGA(2,2) OM2 (VC/F) OM2 NA NA LogNormal VC/F + OMEGA(3,3) OM3 (KA) OM3 NA NA LogNormal KA Message -- Sigma Parameters -- diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run-err.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run-err.html deleted file mode 100644 index 16dd1966..00000000 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run-err.html +++ /dev/null @@ -1,129 +0,0 @@ - -Model Summary: run-err - -Problem: Base one-compartment oral absorption model created from pharos see run004_metadata.json for details. - -Records: 240 | Observations: 210 | Subjects: 30 - -Final OFV: -103.3 - - -Estimation Methods - -- First Order Conditional Estimation with Interaction - - -Heuristic Checks - -[OK] Minimization Successful - -[] Covariance Step Not Run - -[] Eigenvalue Check Not Available - -[OK] No Parameters Near Boundary - -[OK] No Hessian Resets - - - -Theta Parameters - - - - - - - - - - - - - - - - - - - - - - - - - - -
Parameter Estimate Fixed
THETA1 1.241 No
THETA2 40.86 No
THETA3 1.241 No
- - - -Omega Parameters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Parameter Random Effect Estimate Shrinkage (%) Fixed
OMEGA(1,1) ETA1 0.1309 18.98 No
OMEGA(2,2) ETA2 0.1357 4.909 No
OMEGA(3,3) ETA3 0.1 NA Yes
- - - -Sigma Parameters - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Parameter Random Effect Estimate Shrinkage (%) Fixed
SIGMA(1,1) EPS1 0.03635 15.28 No
SIGMA(2,2) EPS2 0.01 NA Yes
- diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run001.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run001.html index 2634ffa6..6c2dc7fd 100644 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run001.html +++ b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run001.html @@ -42,21 +42,21 @@ - THETA1 + TVCL 1.241 0.1129 9.096 No - THETA2 + TVV 40.86 3 7.343 No - THETA3 + TVKA 1.241 0.108 8.697 @@ -83,7 +83,7 @@ - OMEGA(1,1) + OM1 (TVCL) ETA1 0.1309 0.05481 @@ -92,7 +92,7 @@ No - OMEGA(2,2) + OM2 (TVV) ETA2 0.1357 0.03891 @@ -101,7 +101,7 @@ No - OMEGA(3,3) + OM3 (TVKA) ETA3 0.1 NA diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003.html index a839fcec..928223ed 100644 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003.html +++ b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003.html @@ -92,7 +92,7 @@ No - OMEGA(2,1) + OM1,2 (TVCL, TVV) ETA1:ETA2 0.07454 0.03134 diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003b1.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003b1.html index 456dcb90..25e03d09 100644 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003b1.html +++ b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run003b1.html @@ -84,7 +84,7 @@ No - OMEGA(2,1) + OM1,2 (TVCL, TVV) ETA1:ETA2 0.07218 NA diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004-running.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004-running.html index 478d7b7f..24ac0e6d 100644 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004-running.html +++ b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004-running.html @@ -14,15 +14,15 @@ THETA1 THETA2 THETA3 - SIGMA.1.1. - SIGMA.2.1. - SIGMA.2.2. - OMEGA.1.1. - OMEGA.2.1. - OMEGA.2.2. - OMEGA.3.1. - OMEGA.3.2. - OMEGA.3.3. + SIGMA_1_1 + SIGMA_2_1 + SIGMA_2_2 + OMEGA_1_1 + OMEGA_2_1 + OMEGA_2_2 + OMEGA_3_1 + OMEGA_3_2 + OMEGA_3_3 @@ -149,15 +149,15 @@ iteration method - GRD.TVCL. - GRD.TVV. - GRD.TVKA. - GRD.ETA1. - GRD.ETA2. - GRD.EPS1. - GRD.7. - GRD.8. - GRD.9. + TVCL + TVV + TVKA + OM1_TVCL + OM1_2_TVCL_TVV + OM2_TVV + OM3_TVKA + SIGMA_1_1 + SIGMA_2_2 diff --git a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004.html b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004.html index 478d7b7f..24ac0e6d 100644 --- a/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004.html +++ b/tests/testthat/_snaps/hyperion-summary-knit/summary-knit-run004.html @@ -14,15 +14,15 @@ THETA1 THETA2 THETA3 - SIGMA.1.1. - SIGMA.2.1. - SIGMA.2.2. - OMEGA.1.1. - OMEGA.2.1. - OMEGA.2.2. - OMEGA.3.1. - OMEGA.3.2. - OMEGA.3.3. + SIGMA_1_1 + SIGMA_2_1 + SIGMA_2_2 + OMEGA_1_1 + OMEGA_2_1 + OMEGA_2_2 + OMEGA_3_1 + OMEGA_3_2 + OMEGA_3_3 @@ -149,15 +149,15 @@ iteration method - GRD.TVCL. - GRD.TVV. - GRD.TVKA. - GRD.ETA1. - GRD.ETA2. - GRD.EPS1. - GRD.7. - GRD.8. - GRD.9. + TVCL + TVV + TVKA + OM1_TVCL + OM1_2_TVCL_TVV + OM2_TVV + OM3_TVKA + SIGMA_1_1 + SIGMA_2_2 diff --git a/tests/testthat/_snaps/hyperion-summary-print.md b/tests/testthat/_snaps/hyperion-summary-print.md index 9060835e..e8602a5c 100644 --- a/tests/testthat/_snaps/hyperion-summary-print.md +++ b/tests/testthat/_snaps/hyperion-summary-print.md @@ -32,9 +32,9 @@ Parameter Estimate SE RSE (%) Fixed ───────── ──────── ────── ─────── ───── - THETA1 1.241 0.1129 9.096 No - THETA2 40.86 3 7.343 No - THETA3 1.241 0.108 8.697 No + TVCL 1.241 0.1129 9.096 No + TVV 40.86 3 7.343 No + TVKA 1.241 0.108 8.697 No Message -- Omega Parameters -- @@ -43,9 +43,9 @@ Parameter Random Effect Estimate SE RSE (%) Shrinkage (%) Fixed ────────── ───────────── ──────── ─────── ─────── ───────────── ───── - OMEGA(1,1) ETA1 0.1309 0.05481 41.86 18.98 No - OMEGA(2,2) ETA2 0.1357 0.03891 28.68 4.909 No - OMEGA(3,3) ETA3 0.1 NA NA NA Yes + OM1 (TVCL) ETA1 0.1309 0.05481 41.86 18.98 No + OM2 (TVV) ETA2 0.1357 0.03891 28.68 4.909 No + OM3 (TVKA) ETA3 0.1 NA NA NA Yes Message -- Sigma Parameters -- @@ -160,12 +160,12 @@ Output - Parameter Random Effect Estimate SE RSE (%) Shrinkage (%) Fixed - ────────── ───────────── ──────── ─────── ─────── ───────────── ───── - OM1 (TVCL) ETA1 0.1223 0.05036 41.16 13.14 No - OMEGA(2,1) ETA1:ETA2 0.07454 0.03134 42.04 NA No - OM2 (TVV) ETA2 0.1239 0.03675 29.66 4.631 No - OM3 (TVKA) ETA3 0.1224 0.05628 45.97 24.34 No + Parameter Random Effect Estimate SE RSE (%) Shrinkage (%) Fixed + ───────────────── ───────────── ──────── ─────── ─────── ───────────── ───── + OM1 (TVCL) ETA1 0.1223 0.05036 41.16 13.14 No + OM1,2 (TVCL, TVV) ETA1:ETA2 0.07454 0.03134 42.04 NA No + OM2 (TVV) ETA2 0.1239 0.03675 29.66 4.631 No + OM3 (TVKA) ETA3 0.1224 0.05628 45.97 24.34 No Message -- Sigma Parameters -- @@ -221,12 +221,12 @@ Output - Parameter Random Effect Estimate Shrinkage (%) Fixed - ────────── ───────────── ──────── ───────────── ───── - OM1 (TVCL) ETA1 0.1233 13.66 No - OMEGA(2,1) ETA1:ETA2 0.07218 NA No - OM2 (TVV) ETA2 0.1246 4.625 No - OM3 (TVKA) ETA3 0.1239 24.36 No + Parameter Random Effect Estimate Shrinkage (%) Fixed + ───────────────── ───────────── ──────── ───────────── ───── + OM1 (TVCL) ETA1 0.1233 13.66 No + OM1,2 (TVCL, TVV) ETA1:ETA2 0.07218 NA No + OM2 (TVV) ETA2 0.1246 4.625 No + OM3 (TVKA) ETA3 0.1239 24.36 No Message -- Sigma Parameters -- @@ -253,30 +253,30 @@ Output - iteration method THETA1 THETA2 THETA3 SIGMA.1.1. SIGMA.2.1. SIGMA.2.2. OMEGA.1.1. OMEGA.2.1. OMEGA.2.2. OMEGA.3.1. OMEGA.3.2. OMEGA.3.3. - ───────── ────── ────── ────── ────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── - 0 FOCE 1.612 36.18 1.004 0.03574 0 0.006 0.1 0.0001 0.1 0 0 0.1 - 5 FOCE 1.251 40.66 1.246 0.03667 0 0.006033 0.1262 0.000117 0.1325 0 0 0.1067 - 10 FOCE 1.247 40.73 1.24 0.03753 0 0.006265 0.1314 0.0004518 0.1371 0 0 0.114 - 15 FOCE 1.326 40.06 1.212 0.03914 0 0.000918 0.1261 0.07338 0.1228 0 0 0.1207 - 20 FOCE 1.325 40.09 1.21 0.03744 0 0.005545 0.1221 0.07462 0.1239 0 0 0.1223 - 25 FOCE 1.325 40.13 1.211 0.03754 0 0.005267 0.1223 0.07459 0.1239 0 0 0.1224 - 30 FOCE 1.325 40.16 1.212 0.03754 0 0.005272 0.1223 0.07454 0.1239 0 0 0.1224 + iteration method THETA1 THETA2 THETA3 SIGMA_1_1 SIGMA_2_1 SIGMA_2_2 OMEGA_1_1 OMEGA_2_1 OMEGA_2_2 OMEGA_3_1 OMEGA_3_2 OMEGA_3_3 + ───────── ────── ────── ────── ────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── + 0 FOCE 1.612 36.18 1.004 0.03574 0 0.006 0.1 0.0001 0.1 0 0 0.1 + 5 FOCE 1.251 40.66 1.246 0.03667 0 0.006033 0.1262 0.000117 0.1325 0 0 0.1067 + 10 FOCE 1.247 40.73 1.24 0.03753 0 0.006265 0.1314 0.0004518 0.1371 0 0 0.114 + 15 FOCE 1.326 40.06 1.212 0.03914 0 0.000918 0.1261 0.07338 0.1228 0 0 0.1207 + 20 FOCE 1.325 40.09 1.21 0.03744 0 0.005545 0.1221 0.07462 0.1239 0 0 0.1223 + 25 FOCE 1.325 40.13 1.211 0.03754 0 0.005267 0.1223 0.07459 0.1239 0 0 0.1224 + 30 FOCE 1.325 40.16 1.212 0.03754 0 0.005272 0.1223 0.07454 0.1239 0 0 0.1224 Message -- Recent Gradients -- Output - iteration method GRD.TVCL. GRD.TVV. GRD.TVKA. GRD.ETA1. GRD.ETA2. GRD.EPS1. GRD.7. GRD.8. GRD.9. - ───────── ────── ───────── ──────── ────────── ────────── ─────────── ────────── ────────── ───────── ────────── - 0 FOCE 73.89 -28.82 -39.24 -19.1 -0.2895 -15.42 -3.366 -25.08 -1.02 - 5 FOCE 0.726 -0.4298 1.646 -1.356 -0.1662 -1.269 -1.461 -5.953 -0.471 - 10 FOCE -0.292 -0.1409 -0.6106 0.221 -0.1557 0.5171 0.04447 1.326 0.00219 - 15 FOCE -0.476 0.05529 0.08351 0.05463 -0.002578 0.3325 -0.03349 -0.8686 -0.1977 - 20 FOCE 0.07577 0.1036 0.01967 -0.01282 0.0001055 -0.02382 -0.03252 0.08144 0.05834 - 25 FOCE -0.09361 -0.2471 -0.06087 -0.002033 0.0001151 -0.007342 -0.005697 -0.006275 -0.002337 - 30 FOCE -0.002697 0.00215 -0.0007652 -0.0003618 0.000001674 -0.0002712 0.00001258 -0.00347 -0.0002455 + iteration method TVCL TVV TVKA OM1_TVCL OM1_2_TVCL_TVV OM2_TVV OM3_TVKA SIGMA_1_1 SIGMA_2_2 + ───────── ────── ───────── ─────── ────────── ────────── ────────────── ────────── ────────── ───────── ────────── + 0 FOCE 73.89 -28.82 -39.24 -19.1 -0.2895 -15.42 -3.366 -25.08 -1.02 + 5 FOCE 0.726 -0.4298 1.646 -1.356 -0.1662 -1.269 -1.461 -5.953 -0.471 + 10 FOCE -0.292 -0.1409 -0.6106 0.221 -0.1557 0.5171 0.04447 1.326 0.00219 + 15 FOCE -0.476 0.05529 0.08351 0.05463 -0.002578 0.3325 -0.03349 -0.8686 -0.1977 + 20 FOCE 0.07577 0.1036 0.01967 -0.01282 0.0001055 -0.02382 -0.03252 0.08144 0.05834 + 25 FOCE -0.09361 -0.2471 -0.06087 -0.002033 0.0001151 -0.007342 -0.005697 -0.006275 -0.002337 + 30 FOCE -0.002697 0.00215 -0.0007652 -0.0003618 0.000001674 -0.0002712 0.00001258 -0.00347 -0.0002455 # hyperion.nonmem-summary print works for run005 (not_run) @@ -309,87 +309,28 @@ Output - iteration method THETA1 THETA2 THETA3 SIGMA.1.1. SIGMA.2.1. SIGMA.2.2. OMEGA.1.1. OMEGA.2.1. OMEGA.2.2. OMEGA.3.1. OMEGA.3.2. OMEGA.3.3. - ───────── ────── ────── ────── ────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── - 0 FOCE 1.612 36.18 1.004 0.03574 0 0.006 0.1 0.0001 0.1 0 0 0.1 - 5 FOCE 1.251 40.66 1.246 0.03667 0 0.006033 0.1262 0.000117 0.1325 0 0 0.1067 - 10 FOCE 1.247 40.73 1.24 0.03753 0 0.006265 0.1314 0.0004518 0.1371 0 0 0.114 - 15 FOCE 1.326 40.06 1.212 0.03914 0 0.000918 0.1261 0.07338 0.1228 0 0 0.1207 - 20 FOCE 1.325 40.09 1.21 0.03744 0 0.005545 0.1221 0.07462 0.1239 0 0 0.1223 - 25 FOCE 1.325 40.13 1.211 0.03754 0 0.005267 0.1223 0.07459 0.1239 0 0 0.1224 - 30 FOCE 1.325 40.16 1.212 0.03754 0 0.005272 0.1223 0.07454 0.1239 0 0 0.1224 + iteration method THETA1 THETA2 THETA3 SIGMA_1_1 SIGMA_2_1 SIGMA_2_2 OMEGA_1_1 OMEGA_2_1 OMEGA_2_2 OMEGA_3_1 OMEGA_3_2 OMEGA_3_3 + ───────── ────── ────── ────── ────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── ───────── + 0 FOCE 1.612 36.18 1.004 0.03574 0 0.006 0.1 0.0001 0.1 0 0 0.1 + 5 FOCE 1.251 40.66 1.246 0.03667 0 0.006033 0.1262 0.000117 0.1325 0 0 0.1067 + 10 FOCE 1.247 40.73 1.24 0.03753 0 0.006265 0.1314 0.0004518 0.1371 0 0 0.114 + 15 FOCE 1.326 40.06 1.212 0.03914 0 0.000918 0.1261 0.07338 0.1228 0 0 0.1207 + 20 FOCE 1.325 40.09 1.21 0.03744 0 0.005545 0.1221 0.07462 0.1239 0 0 0.1223 + 25 FOCE 1.325 40.13 1.211 0.03754 0 0.005267 0.1223 0.07459 0.1239 0 0 0.1224 + 30 FOCE 1.325 40.16 1.212 0.03754 0 0.005272 0.1223 0.07454 0.1239 0 0 0.1224 Message -- Recent Gradients -- Output - iteration method GRD.TVCL. GRD.TVV. GRD.TVKA. GRD.ETA1. GRD.ETA2. GRD.EPS1. GRD.7. GRD.8. GRD.9. - ───────── ────── ───────── ──────── ────────── ────────── ─────────── ────────── ────────── ───────── ────────── - 0 FOCE 73.89 -28.82 -39.24 -19.1 -0.2895 -15.42 -3.366 -25.08 -1.02 - 5 FOCE 0.726 -0.4298 1.646 -1.356 -0.1662 -1.269 -1.461 -5.953 -0.471 - 10 FOCE -0.292 -0.1409 -0.6106 0.221 -0.1557 0.5171 0.04447 1.326 0.00219 - 15 FOCE -0.476 0.05529 0.08351 0.05463 -0.002578 0.3325 -0.03349 -0.8686 -0.1977 - 20 FOCE 0.07577 0.1036 0.01967 -0.01282 0.0001055 -0.02382 -0.03252 0.08144 0.05834 - 25 FOCE -0.09361 -0.2471 -0.06087 -0.002033 0.0001151 -0.007342 -0.005697 -0.006275 -0.002337 - 30 FOCE -0.002697 0.00215 -0.0007652 -0.0003618 0.000001674 -0.0002712 0.00001258 -0.00347 -0.0002455 - -# hyperion.nonmem-summary print fails gracefully for ill-formatted comments - - Code - print(mod_sum) - Message - - - -- Model Summary: run-err ------------------------------------------------------ - Problem: Base one-compartment oral absorption model created from pharos see - run004_metadata.json for details. - Records: 240 | Observations: 210 | Subjects: 30 - Final OFV: -103.3 - - -- Estimation Methods -- - - * First Order Conditional Estimation with Interaction - - -- Heuristic Checks -- - - [OK] Minimization Successful - [!] Covariance Step Not Run - [!] Eigenvalue Check Not Available - [OK] No Parameters Near Boundary - [OK] No Hessian Resets - Output - - Message - - -- Theta Parameters -- - - Output - - Parameter Estimate Fixed - ───────── ──────── ───── - THETA1 1.241 No - THETA2 40.86 No - THETA3 1.241 No - - Message - -- Omega Parameters -- - - Output - - Parameter Random Effect Estimate Shrinkage (%) Fixed - ────────── ───────────── ──────── ───────────── ───── - OMEGA(1,1) ETA1 0.1309 18.98 No - OMEGA(2,2) ETA2 0.1357 4.909 No - OMEGA(3,3) ETA3 0.1 NA Yes - - Message - -- Sigma Parameters -- - - Output - - Parameter Random Effect Estimate Shrinkage (%) Fixed - ────────── ───────────── ──────── ───────────── ───── - SIGMA(1,1) EPS1 0.03635 15.28 No - SIGMA(2,2) EPS2 0.01 NA Yes + iteration method TVCL TVV TVKA OM1_TVCL OM1_2_TVCL_TVV OM2_TVV OM3_TVKA SIGMA_1_1 SIGMA_2_2 + ───────── ────── ───────── ─────── ────────── ────────── ────────────── ────────── ────────── ───────── ────────── + 0 FOCE 73.89 -28.82 -39.24 -19.1 -0.2895 -15.42 -3.366 -25.08 -1.02 + 5 FOCE 0.726 -0.4298 1.646 -1.356 -0.1662 -1.269 -1.461 -5.953 -0.471 + 10 FOCE -0.292 -0.1409 -0.6106 0.221 -0.1557 0.5171 0.04447 1.326 0.00219 + 15 FOCE -0.476 0.05529 0.08351 0.05463 -0.002578 0.3325 -0.03349 -0.8686 -0.1977 + 20 FOCE 0.07577 0.1036 0.01967 -0.01282 0.0001055 -0.02382 -0.03252 0.08144 0.05834 + 25 FOCE -0.09361 -0.2471 -0.06087 -0.002033 0.0001151 -0.007342 -0.005697 -0.006275 -0.002337 + 30 FOCE -0.002697 0.00215 -0.0007652 -0.0003618 0.000001674 -0.0002712 0.00001258 -0.00347 -0.0002455 diff --git a/tests/testthat/_snaps/hyperion-tree-knit/tree-knit.html b/tests/testthat/_snaps/hyperion-tree-knit/tree-knit.html index 7e2a0607..5387fc48 100644 --- a/tests/testthat/_snaps/hyperion-tree-knit/tree-knit.html +++ b/tests/testthat/_snaps/hyperion-tree-knit/tree-knit.html @@ -3,6 +3,6 @@ ℹ️ Models: 3 -- base - Base population PK model - - run001 - Run 1 - - run002 - Run 2 with covariate effects +- base Base population PK model + - run001 Run 1 + - run002 Run 2 with covariate effects diff --git a/tests/testthat/_snaps/hyperion-tree-print.md b/tests/testthat/_snaps/hyperion-tree-print.md index fe1cbc10..9ff5e414 100644 --- a/tests/testthat/_snaps/hyperion-tree-print.md +++ b/tests/testthat/_snaps/hyperion-tree-print.md @@ -9,7 +9,40 @@ i Models: 3 Output - base - Base population PK model - \-run001 - Run 1 - \-run002 - Run 2 with covariate effects + base Base population PK model + \-run001 Run 1 + \-run002 Run 2 with covariate effects + +# hyperion_nonmem_tree print honors verbose attr + + Code + print(tree) + Message + + + -- Hyperion Model Tree --------------------------------------------------------- + i Models: 2 + + Output + Model Parent Description Tags Model Hash Dataset Hash + ───────────────────────────────────────────────────────────────────────── + base Base population PK model base f873a13c... 8d8189cf... + run001 base Adding COV step + +# hyperion_nonmem_tree verbose print renders 6-column table + + Code + print(tree, verbose = TRUE) + Message + + + -- Hyperion Model Tree --------------------------------------------------------- + i Models: 3 + + Output + Model Parent Description Tags Model Hash Dataset Hash + ──────────────────────────────────────────────────────────────────────────────────────────── + base Base population PK model base f873a13c... 8d8189cf... + run001 base Adding COV step, unfixing CL covariates, unfixed 1a0f07a1... 8d8189cf... + run002 run001 Not yet run diff --git a/tests/testthat/_snaps/lineage.md b/tests/testthat/_snaps/lineage.md new file mode 100644 index 00000000..337b8cef --- /dev/null +++ b/tests/testthat/_snaps/lineage.md @@ -0,0 +1,84 @@ +# get_model_lineage() returns the whole project tree + + Code + get_model_lineage() + Message + + + -- Hyperion Model Tree --------------------------------------------------------- + i Models: 9 + + Output + extdata/models/onecmt/run001 Base model + +-extdata/models/onecmt/run002 Adding COV step, unfixing eps(2) + | +-extdata/models/onecmt/run002a Some description about what makes run002a ... + | +-extdata/models/onecmt/run002b001 Jittering initial sigma estimates, usin... + | \-extdata/models/onecmt/run003 Jittering initial estimates + | +-extdata/models/onecmt/run003b1 Updating run003 to 003b1 with jittered ... + | \-extdata/models/onecmt/run003b2 Updating run003 with mod object + +-extdata/models/onecmt/run004 Updating run001 to run004 with jittered param... + \-extdata/models/onecmt/run005 Updating run001 to run004 with jittered param... + +# get_model_lineage(model) returns the model's full lineage + + Code + get_model_lineage("extdata/models/onecmt/run003.mod") + Message + + + -- Hyperion Model Tree --------------------------------------------------------- + i Models: 5 + + Output + extdata/models/onecmt/run001 Base model + \-extdata/models/onecmt/run002 Adding COV step, unfixing eps(2) + \-extdata/models/onecmt/run003 Jittering initial estimates + +-extdata/models/onecmt/run003b1 Updating run003 to 003b1 with jittered ... + \-extdata/models/onecmt/run003b2 Updating run003 with mod object + +# get_model_lineage(from, to) slices between two models + + Code + get_model_lineage(from = "extdata/models/onecmt/run001.mod", to = "extdata/models/onecmt/run003b1.mod") + Message + + + -- Hyperion Model Tree --------------------------------------------------------- + i Models: 4 + + Output + extdata/models/onecmt/run001 Base model + \-extdata/models/onecmt/run002 Adding COV step, unfixing eps(2) + \-extdata/models/onecmt/run003 Jittering initial estimates + \-extdata/models/onecmt/run003b1 Updating run003 to 003b1 with jittered ... + +# lineage helpers return project-relative paths + + Code + get_model_ancestors("extdata/models/onecmt/run003b1.mod") + Output + [1] "extdata/models/onecmt/run001.mod" "extdata/models/onecmt/run002.mod" + [3] "extdata/models/onecmt/run003.mod" "extdata/models/onecmt/run003b1.mod" + +--- + + Code + get_model_descendants("extdata/models/onecmt/run001.mod") + Output + [1] "extdata/models/onecmt/run002.mod" + [2] "extdata/models/onecmt/run002a.mod" + [3] "extdata/models/onecmt/run002b001.mod" + [4] "extdata/models/onecmt/run003.mod" + [5] "extdata/models/onecmt/run003b1.mod" + [6] "extdata/models/onecmt/run003b2.mod" + [7] "extdata/models/onecmt/run004.mod" + [8] "extdata/models/onecmt/run005.mod" + +--- + + Code + are_models_in_lineage("extdata/models/onecmt/run001.mod", + "extdata/models/onecmt/run003b1.mod") + Output + [1] TRUE + diff --git a/tests/testthat/setup-config-dir.R b/tests/testthat/setup-config-dir.R new file mode 100644 index 00000000..205fddfe --- /dev/null +++ b/tests/testthat/setup-config-dir.R @@ -0,0 +1,9 @@ +# Pin the project root for the test session to the install root (where +# inst/pharos.toml lives). Without this, pharos's CWD-walk lands on +# tests/pharos.toml and treats `tests/` as the project root — fixtures +# under inst/extdata/ are then "outside the project root" for +# `to_root_relative` and every read_model() call errors. +old_opts <- options( + hyperion.config_dir = system.file(package = "hyperion") +) +withr::defer(options(old_opts), teardown_env()) diff --git a/tests/testthat/test-comments-classes-validation.R b/tests/testthat/test-comments-classes-validation.R index 3c1bca06..16a1212c 100644 --- a/tests/testthat/test-comments-classes-validation.R +++ b/tests/testthat/test-comments-classes-validation.R @@ -34,113 +34,3 @@ test_that("ModelComments enforces comment class types", { ) }) -test_that("ModelComments allows unnamed omega duplicates from model files", { - mod_path <- system.file( - "extdata", - "models", - "run-duplicate-omega-names.mod", - package = "hyperion" - ) - mod <- read_model(mod_path) - param_names <- get_model_parameter_names(mod) - comments_data <- hyperion:::extract_comments(mod) - comments <- hyperion:::parse_comments( - param_names, - comments_data$parsed, - comments_data$raw, - mod_path - ) - omega_comments <- comments[grepl("^OMEGA", names(comments))] - - expect_no_error(ModelComments(omega = omega_comments)) -}) - -test_that("ModelComments renames duplicate omega names to name-associated_theta", { - theta1 <- ThetaComment(nonmem_name = "THETA1", name = "CL") - theta2 <- ThetaComment(nonmem_name = "THETA2", name = "V") - theta3 <- ThetaComment(nonmem_name = "THETA3", name = "KA") - - # All three omegas have the same name "IIV" but different associated_theta - omega1 <- OmegaComment( - nonmem_name = "OMEGA(1,1)", - name = "IIV", - associated_theta = "CL" - ) - omega2 <- OmegaComment( - nonmem_name = "OMEGA(2,2)", - name = "IIV", - associated_theta = "V" - ) - omega3 <- OmegaComment( - nonmem_name = "OMEGA(3,3)", - name = "IIV", - associated_theta = "KA" - ) - - # Set up sources to test audit trail - attr(omega1, "sources") <- list(name = "test.lst") - attr(omega2, "sources") <- list(name = "test.lst") - attr(omega3, "sources") <- list(name = "test.lst") - - info <- ModelComments( - theta = list(THETA1 = theta1, THETA2 = theta2, THETA3 = theta3), - omega = list( - `OMEGA(1,1)` = omega1, - `OMEGA(2,2)` = omega2, - `OMEGA(3,3)` = omega3 - ) - ) - - # All names should be renamed to include associated_theta - expect_equal(info@omega[["OMEGA(1,1)"]]@name, "IIV-CL") - expect_equal(info@omega[["OMEGA(2,2)"]]@name, "IIV-V") - expect_equal(info@omega[["OMEGA(3,3)"]]@name, "IIV-KA") - - # Audit should show "renamed from" source - audit <- audit_parameter_info(info) - expect_true(all(grepl("renamed from", audit$omega$name))) -}) - -test_that("raw comment parsing renames duplicate omega names", { - mod_files <- c( - "run-duplicate-omega-names-with-theta.mod", - "run-duplicate-omega-names-space.mod" - ) - - for (mod_file in mod_files) { - mod_path <- system.file("extdata", "models", mod_file, package = "hyperion") - mod <- read_model(mod_path) - param_names <- get_model_parameter_names(mod) - comments_data <- hyperion:::extract_comments(mod) - comments <- hyperion:::parse_comments( - param_names, - comments_data$parsed, - comments_data$raw, - mod_path - ) - - theta_comments <- comments[grepl("^THETA", names(comments))] - omega_comments <- comments[grepl("^OMEGA", names(comments))] - - info <- ModelComments(theta = theta_comments, omega = omega_comments) - - # All omega names should be unique after renaming - omega_names <- vapply( - info@omega, - function(c) c@name, - character(1) - ) - expect_equal( - length(omega_names), - length(unique(omega_names)), - info = paste("Failed for:", mod_file) - ) - - # Audit should show "renamed from" for all omega names - audit <- audit_parameter_info(info) - expect_true( - all(grepl("renamed from", audit$omega$name)), - info = paste("Failed for:", mod_file) - ) - } -}) diff --git a/tests/testthat/test-comments-query.R b/tests/testthat/test-comments-query.R index 8974a6b3..30ab244b 100644 --- a/tests/testthat/test-comments-query.R +++ b/tests/testthat/test-comments-query.R @@ -34,7 +34,7 @@ test_that("get_parameter_names uses associated_theta when name is NA", { ) omega11 <- OmegaComment( nonmem_name = "OMEGA(1,1)", - name = NA_character_, + name = "CL/F", associated_theta = "CL/F" ) diff --git a/tests/testthat/test-comments-validation.R b/tests/testthat/test-comments-validation.R deleted file mode 100644 index bb0822d6..00000000 --- a/tests/testthat/test-comments-validation.R +++ /dev/null @@ -1,65 +0,0 @@ -test_that("omega associated_theta matches theta names case-insensitively", { - theta1 <- ThetaComment( - nonmem_name = "THETA1", - name = "CL/F" - ) - omega11 <- OmegaComment( - nonmem_name = "OMEGA(1,1)", - name = "IIV", - associated_theta = "cl/f" - ) - - info <- ModelComments( - theta = list(THETA1 = theta1), - omega = list(`OMEGA(1,1)` = omega11), - sigma = list() - ) - expect_equal(info@omega$`OMEGA(1,1)`@associated_theta, "CL/F") - - theta1 <- ThetaComment( - nonmem_name = "THETA1", - name = "cl/f" - ) - omega11 <- OmegaComment( - nonmem_name = "OMEGA(1,1)", - name = "IIV", - associated_theta = "CL/F" - ) - - info <- ModelComments( - theta = list(THETA1 = theta1), - omega = list(`OMEGA(1,1)` = omega11), - sigma = list() - ) - expect_equal(info@omega$`OMEGA(1,1)`@associated_theta, "cl/f") -}) - -test_that("omega associated_theta matches theta names by stripping suffix", { - theta1 <- ThetaComment( - nonmem_name = "THETA1", - name = "CL/F" - ) - theta2 <- ThetaComment( - nonmem_name = "THETA2", - name = "VC/F" - ) - omega11 <- OmegaComment( - nonmem_name = "OMEGA(1,1)", - name = "IIV-CL", - associated_theta = "CL" - ) - omega22 <- OmegaComment( - nonmem_name = "OMEGA(2,2)", - name = "IIV-VC", - associated_theta = "VC" - ) - - info <- ModelComments( - theta = list(THETA1 = theta1, THETA2 = theta2), - omega = list(`OMEGA(1,1)` = omega11, `OMEGA(2,2)` = omega22), - sigma = list() - ) - - expect_equal(info@omega$`OMEGA(1,1)`@associated_theta, "CL/F") - expect_equal(info@omega$`OMEGA(2,2)`@associated_theta, "VC/F") -}) diff --git a/tests/testthat/test-extract_raw_omega_parts.R b/tests/testthat/test-extract_raw_omega_parts.R deleted file mode 100644 index a896c97e..00000000 --- a/tests/testthat/test-extract_raw_omega_parts.R +++ /dev/null @@ -1,93 +0,0 @@ -test_that("extract_raw_omega_parts parses comments correctly", { - # Single word after prefix stripping - no theta ref, just name - parts <- extract_raw_omega_parts("OMEGA1: CL :EXP") - expect_equal(parts$name, "CL") - expect_equal(parts$parameterization, "EXP") - - parts <- extract_raw_omega_parts("1: CL :EXP") - expect_equal(parts$name, "CL") - expect_equal(parts$parameterization, "EXP") - - # Name is prefix only, associated_theta stored separately - parts <- extract_raw_omega_parts("1: OM2,1 CL-VC ; normal") - expect_equal(parts$name, "OM2,1") - expect_equal(parts$parameterization, "normal") - expect_equal(parts$associated_theta, c("CL", "VC")) - - parts <- extract_raw_omega_parts("1: OM2,1 CL/VC ; normal") - expect_equal(parts$name, "OM2,1") - expect_equal(parts$parameterization, "normal") - expect_equal(parts$associated_theta, c("CL", "VC")) - - parts <- extract_raw_omega_parts("OM2,1 CL,VC ; normal") - expect_equal(parts$name, "OM2,1") - expect_equal(parts$parameterization, "normal") - expect_equal(parts$associated_theta, c("CL", "VC")) - - # Single word - no theta ref - parts <- extract_raw_omega_parts("OMEGA1: CL ; exp") - expect_equal(parts$name, "CL") - expect_equal(parts$parameterization, "exp") - - # prefix + theta ref = prefix in name, theta in associated_theta - parts <- extract_raw_omega_parts("eta1 CL ; exp") - expect_equal(parts$name, "eta1") - expect_equal(parts$parameterization, "exp") - expect_equal(parts$associated_theta, "CL") - - parts <- extract_raw_omega_parts( - "OMEGA(2,1) CL/F-V2/F", - known_thetas = c("CL/F", "V2/F") - ) - expect_equal(parts$name, NULL) - expect_equal(parts$associated_theta, c("CL/F", "V2/F")) - expect_equal(parts$parameterization, NULL) - - parts <- extract_raw_omega_parts( - "OMEGA(2,1) Cov CL/F-V2/F", - known_thetas = c("CL/F", "V2/F") - ) - expect_equal(parts$name, "Cov") - expect_equal(parts$associated_theta, c("CL/F", "V2/F")) - expect_equal(parts$parameterization, NULL) - - parts <- extract_raw_omega_parts( - "CL/F-V2/F", - known_thetas = c("CL/F", "V2/F") - ) - expect_equal(parts$name, NULL) - expect_equal(parts$associated_theta, c("CL/F", "V2/F")) - expect_equal(parts$parameterization, NULL) - - parts <- extract_raw_omega_parts( - "CL/F:V2/F", - known_thetas = c("CL/F", "V2/F") - ) - expect_equal(parts$name, NULL) - expect_equal(parts$associated_theta, c("CL/F", "V2/F")) - expect_equal(parts$parameterization, NULL) -}) - -test_that("parse_raw_omega_comment handles off-diagonal pair and diagonal hyphenated name", { - known_thetas <- c("CL/F", "V2/F") - - # Off-diagonal covariance-style comment - offdiag <- parse_raw_omega_comment( - "OMEGA(2,1)", - NULL, - "Cov CL/F-V2/F", - known_thetas = known_thetas - ) - expect_equal(offdiag@name, "Cov") - expect_equal(offdiag@associated_theta, c("CL/F", "V2/F")) - - # Diagonal standard comment should remain name + single theta - diag <- parse_raw_omega_comment( - "OMEGA(1,1)", - NULL, - "IIV-CL/F", - known_thetas = known_thetas - ) - expect_equal(diag@name, "IIV") - expect_equal(diag@associated_theta, "CL/F") -}) diff --git a/tests/testthat/test-extract_raw_sigma_parts.R b/tests/testthat/test-extract_raw_sigma_parts.R deleted file mode 100644 index 4b6f4a86..00000000 --- a/tests/testthat/test-extract_raw_sigma_parts.R +++ /dev/null @@ -1,61 +0,0 @@ -test_that("extract_raw_sigma_parts accepts numbered prefixes", { - parts <- extract_raw_sigma_parts("1: Proportional error") - expect_equal(parts$name, "Proportional") - expect_equal(parts$parameterization, NULL) -}) - -test_that("extract_raw_sigma_parts handles numbered name comments", { - parts <- extract_raw_sigma_parts("11 PropErr ;Proportional") - expect_equal(parts$name, "PropErr") - expect_equal(parts$parameterization, "Proportional") -}) - -test_that("extract_raw_sigma_parts handles numbered name comments", { - parts <- extract_raw_sigma_parts("11 PropErr :Proportional") - expect_equal(parts$name, "PropErr") - expect_equal(parts$parameterization, "Proportional") -}) - -test_that("extract_raw_sigma_parts handles numbered name comments", { - parts <- extract_raw_sigma_parts("SIGMA2 AddErr :AddErr") - expect_equal(parts$name, "AddErr") - expect_equal(parts$parameterization, "AddErr") -}) - -test_that("extract_raw_sigma_parts captures units in parentheses or brackets", { - parts <- extract_raw_sigma_parts("22 AddErr (CONC)") - expect_equal(parts$name, "AddErr") - expect_equal(parts$unit, "CONC") - expect_equal(parts$parameterization, NULL) - - parts <- extract_raw_sigma_parts("AddErr [ng/mL] :ADD") - expect_equal(parts$name, "AddErr") - expect_equal(parts$unit, "ng/mL") - expect_equal(parts$parameterization, "ADD") - - parts <- extract_raw_sigma_parts("AddErr ;AddErr (ng/mL)") - expect_equal(parts$name, "AddErr") - expect_equal(parts$unit, "ng/mL") - expect_equal(parts$parameterization, "AddErr") - - parts <- extract_raw_sigma_parts("11 PropErr :Proportional [prop]") - expect_equal(parts$name, "PropErr") - expect_equal(parts$unit, "prop") - expect_equal(parts$parameterization, "Proportional") - - parts <- extract_raw_sigma_parts("22 AddErr :AddErr [ng/mL]") - expect_equal(parts$name, "AddErr") - expect_equal(parts$unit, "ng/mL") - expect_equal(parts$parameterization, "AddErr") - - parts <- extract_raw_sigma_parts("11 PropErr ;Proportional [prop]") - expect_equal(parts$name, "PropErr") - expect_equal(parts$unit, "prop") - expect_equal(parts$parameterization, "Proportional") - - parts <- extract_raw_sigma_parts("22 AddErr ;AddErr [ng/mL]") - expect_equal(parts$name, "AddErr") - expect_equal(parts$unit, "ng/mL") - expect_equal(parts$parameterization, "AddErr") - -}) diff --git a/tests/testthat/test-extract_raw_theta_parts.R b/tests/testthat/test-extract_raw_theta_parts.R deleted file mode 100644 index 48323c36..00000000 --- a/tests/testthat/test-extract_raw_theta_parts.R +++ /dev/null @@ -1,62 +0,0 @@ -test_that("extract_raw_theta_parts parses comments correctly", { - parts <- extract_raw_theta_parts("THETA1: CL (L/day) ; exp") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "exp") - - parts <- extract_raw_theta_parts("1: CL (L/day) ; exp") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "exp") - - parts <- extract_raw_theta_parts("1 CL (L/day) ; exp") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "exp") - - parts <- extract_raw_theta_parts("CL (L/day) ; exp") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "exp") - - parts <- extract_raw_theta_parts("CL ;exp") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, NULL) - expect_equal(parts$parameterization, "exp") - - parts <- extract_raw_theta_parts("THETA1: CL (L/day) :EXP") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "EXP") - - parts <- extract_raw_theta_parts("THETA1 CL (L/day) :LOG") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "LOG") - - parts <- extract_raw_theta_parts("THETA6 RUV :ADD") - expect_equal(parts$name, "RUV") - expect_equal(parts$unit, NULL) - expect_equal(parts$parameterization, "ADD") - - parts <- extract_raw_theta_parts("THETA1 CL [L/day] :LOG") - expect_equal(parts$name, "CL") - expect_equal(parts$unit, "L/day") - expect_equal(parts$parameterization, "LOG") - - parts <- extract_raw_theta_parts(" 5 kon (1/(mg*hr))") - expect_equal(parts$name, "kon") - expect_equal(parts$unit, "1/(mg*hr)") - - parts <- extract_raw_theta_parts(" 5 kon [1/(mg*hr)]") - expect_equal(parts$name, "kon") - expect_equal(parts$unit, "1/(mg*hr)") - - parts <- extract_raw_theta_parts(" 5 kon (1/[mg*hr])") - expect_equal(parts$name, "kon") - expect_equal(parts$unit, "1/[mg*hr]") - - parts <- extract_raw_theta_parts(" 5 kon [1/[mg*hr]]") - expect_equal(parts$name, "kon") - expect_equal(parts$unit, "1/[mg*hr]") -}) diff --git a/tests/testthat/test-format-omega-display-name.R b/tests/testthat/test-format-omega-display-name.R deleted file mode 100644 index bda69aed..00000000 --- a/tests/testthat/test-format-omega-display-name.R +++ /dev/null @@ -1,125 +0,0 @@ -test_that("format_omega_display_name avoids duplicate theta info", { - # Theta already in name via hyphen - no duplication - expect_equal( - format_omega_display_name("IIV-CL", "CL"), - "IIV-CL" - ) - - # Theta already in name via slash - no duplication - expect_equal( - format_omega_display_name("IIV-CL/F", "CL/F"), - "IIV-CL/F" - ) - - # Theta already in name via slash - no duplication - expect_equal( - format_omega_display_name("IIV-CL", "CL/F"), - "IIV-CL" - ) - - # Theta not in name - appends it - expect_equal( - format_omega_display_name("IIV", "CL"), - "IIV CL" - ) - - # Multiple thetas, none present - expect_equal( - format_omega_display_name("COV", c("CL", "V")), - "COV CL, V" - ) - - # Multiple thetas, some present - expect_equal( - format_omega_display_name("IIV-CL", c("CL", "V")), - "IIV-CL V" - ) - - # With custom labels - label already present - expect_equal( - format_omega_display_name("IIV-Clearance", "CL", c(CL = "Clearance")), - "IIV-Clearance" - ) - - # With custom labels that include spaces - label already present - expect_equal( - format_omega_display_name( - "IIV CL/F Scaling", - "CLF", - c("CLF" = "CL/F Scaling") - ), - "IIV CL/F Scaling" - ) - - # With custom labels - appends label not name - expect_equal( - format_omega_display_name("IIV", "CL", c(CL = "Clearance")), - "IIV Clearance" - ) - - # NULL associated_theta returns name unchanged - expect_equal( - format_omega_display_name("IIV", NULL), - "IIV" - ) - - # Empty associated_theta returns name unchanged - expect_equal( - format_omega_display_name("IIV", character(0)), - "IIV" - ) -}) - -test_that("format_omega_display_name matches theta roots after stripping TV/ETA prefix", { - # CL in name matches TVCL theta (TVCL -> CL) - expect_equal( - format_omega_display_name("IIV-CL", "TVCL"), - "IIV-CL" - ) - - # Vc in name matches TVVC theta (TVVC -> VC, case-insensitive) - expect_equal( - format_omega_display_name("IIV-Vc", "TVVC"), - "IIV-Vc" - ) - - # KA in name matches TVKA theta (TVKA -> KA) - expect_equal( - format_omega_display_name("IIV-KA", "TVKA"), - "IIV-KA" - ) - - # Multiple TV-prefixed thetas - expect_equal( - format_omega_display_name("COV-CL-V", c("TVCL", "TVV")), - "COV-CL-V" - ) - - # Partial match - only CL present, V missing (TVV -> V, not in name) - expect_equal( - format_omega_display_name("IIV-CL", c("TVCL", "TVV")), - "IIV-CL TVV" - ) - - # ETA prefix also stripped - expect_equal( - format_omega_display_name("IIV-CL", "ETACL"), - "IIV-CL" - ) -}) - - -test_that("renaming work for off-diags", { - model_dir <- system.file("extdata", "models", "onecmt", package = "hyperion") - - mod <- read_model(file.path(model_dir, "run003.mod")) - info <- get_model_parameter_info(mod) - - display_name <- format_omega_display_name( - info@omega$`OMEGA(2,1)`@name, - info@omega$`OMEGA(2,1)`@associated_theta - ) - expect_equal(display_name, "OM1,2 TVCL, TVV") - expect_equal(info@omega$`OMEGA(2,1)`@name, "OM1,2") - expect_equal(info@omega$`OMEGA(2,1)`@associated_theta, c("TVCL", "TVV")) -}) diff --git a/tests/testthat/test-get_model_parameter_names.R b/tests/testthat/test-get_model_parameter_names.R deleted file mode 100644 index fd3ae3f5..00000000 --- a/tests/testthat/test-get_model_parameter_names.R +++ /dev/null @@ -1,44 +0,0 @@ -test_that("get_model_parameter_names works for typed comments", { - mod_dir <- system.file("extdata", "models", "onecmt", package = "hyperion") - - # run1 has incorrect type1 comments so nothing is parsed - run1 <- read_model(file.path(mod_dir, "run001.mod")) - expect_equal(get_comment_type(), "type1") - - n1 <- get_model_parameter_names(run1) - expect_equal(all(n1 == ""), TRUE) - - # run2 has correct type1 comments for THETA/OMEGA - # so parameter name should have non-empty values. - # Sigma is incorrect and will be empty - run2 <- read_model(file.path(mod_dir, "run002.mod")) - n2 <- get_model_parameter_names(run2) - expect_equal(any(n2 != ""), TRUE) - expect_equal(n2$THETA1, "TVCL") - expect_equal(n2$THETA2, "TVV") - expect_equal(n2$`SIGMA(1,1)`, "") - expect_equal(n2$`SIGMA(2,2)`, "") -}) - -test_that("get_parameter_names works for all comments", { - mod_dir <- system.file("extdata", "models", "onecmt", package = "hyperion") - - # run1 has incorrect type1 comments so nothing is parsed - run1 <- read_model(file.path(mod_dir, "run001.mod")) - n1 <- get_parameter_names(run1) - expect_equal(n1["THETA1", "name"], "TVCL") - expect_equal(n1["THETA2", "name"], "TVV") - - # run2 has correct type1 comments for THETA/OMEGA - # so parameter name should have non-empty values. - # Sigma is incorrect and will be empty, OMEGA is - # processed differently ((theta) vs , theta) - run2 <- read_model(file.path(mod_dir, "run002.mod")) - n2 <- get_parameter_names(run2) - n2_mp <- get_model_parameter_names(run2) - for (p in rownames(n2)) { - if (grepl("^THETA", p)) { - expect_equal(n2[p, "name"], n2_mp[[p]]) - } - } -}) diff --git a/tests/testthat/test-hyperion-summary-knit.R b/tests/testthat/test-hyperion-summary-knit.R index 04c0afb4..e3e47ce2 100644 --- a/tests/testthat/test-hyperion-summary-knit.R +++ b/tests/testthat/test-hyperion-summary-knit.R @@ -33,18 +33,3 @@ test_that("hyperion.nonmem-summary knit_print works for run004 (running)", { }) -test_that("hyperion.nonmem-summary knit_print fails gracefully for ill-formatted comments", { - # Temporarily remove type1 so raw comment parsing is exercised - config_path <- testthat::test_path("..", "pharos.toml") - original <- readLines(config_path) - withr::defer(writeLines(original, config_path)) - writeLines(gsub('type = "type1"', '', original), config_path) - - mod_path <- system.file("extdata", "models", "run-error-names", "run-err.mod", package = "hyperion") - mod <- read_model(mod_path) - expect_warning( - mod_sum <- summary(mod), - "Could not apply parameter names from model comments." - ) - snapshot_knit_html(mod_sum, "summary-knit-run-err") -}) diff --git a/tests/testthat/test-hyperion-summary-print.R b/tests/testthat/test-hyperion-summary-print.R index ae7866df..608e8693 100644 --- a/tests/testthat/test-hyperion-summary-print.R +++ b/tests/testthat/test-hyperion-summary-print.R @@ -31,19 +31,3 @@ test_that("hyperion.nonmem-summary print works for run004 (running)", { expect_snapshot(print(mod_sum)) }) -test_that("hyperion.nonmem-summary print fails gracefully for ill-formatted comments", { - # Temporarily remove type1 so raw comment parsing is exercised - config_path <- testthat::test_path("..", "pharos.toml") - original <- readLines(config_path) - withr::defer(writeLines(original, config_path)) - writeLines(gsub('type = "type1"', '', original), config_path) - - mod_path <- system.file("extdata", "models", "run-error-names", "run-err.mod", package = "hyperion") - mod <- read_model(mod_path) - - expect_warning( - mod_sum <- summary(mod), - "Could not apply parameter names from model comments." - ) - expect_snapshot(print(mod_sum)) -}) diff --git a/tests/testthat/test-hyperion-tree-knit.R b/tests/testthat/test-hyperion-tree-knit.R index af99fd38..ad1e6cf5 100644 --- a/tests/testthat/test-hyperion-tree-knit.R +++ b/tests/testthat/test-hyperion-tree-knit.R @@ -2,17 +2,32 @@ test_that("hyperion_nonmem_tree knit_print works", { tree <- structure( list( nodes = list( - "base.mod" = list( - based_on = list(), - description = "Base population PK model" + list( + name = "base.mod", + model = list( + based_on = list(), + description = "Base population PK model", + tags = list() + ), + run = NULL ), - "run001.mod" = list( - based_on = list("base.mod"), - description = "Run 1" + list( + name = "run001.mod", + model = list( + based_on = list("base.mod"), + description = "Run 1", + tags = list() + ), + run = NULL ), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Run 2 with covariate effects" + list( + name = "run002.mod", + model = list( + based_on = list("run001.mod"), + description = "Run 2 with covariate effects", + tags = list() + ), + run = NULL ) ) ), diff --git a/tests/testthat/test-hyperion-tree-print.R b/tests/testthat/test-hyperion-tree-print.R index 8607e386..19dbebf6 100644 --- a/tests/testthat/test-hyperion-tree-print.R +++ b/tests/testthat/test-hyperion-tree-print.R @@ -2,17 +2,32 @@ test_that("hyperion_nonmem_tree print works", { tree <- structure( list( nodes = list( - "base.mod" = list( - based_on = list(), - description = "Base population PK model" + list( + name = "base.mod", + model = list( + based_on = list(), + description = "Base population PK model", + tags = list() + ), + run = NULL ), - "run001.mod" = list( - based_on = list("base.mod"), - description = "Run 1" + list( + name = "run001.mod", + model = list( + based_on = list("base.mod"), + description = "Run 1", + tags = list() + ), + run = NULL ), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Run 2 with covariate effects" + list( + name = "run002.mod", + model = list( + based_on = list("run001.mod"), + description = "Run 2 with covariate effects", + tags = list() + ), + run = NULL ) ) ), @@ -20,3 +35,80 @@ test_that("hyperion_nonmem_tree print works", { ) expect_snapshot(print(tree)) }) + +test_that("hyperion_nonmem_tree print honors verbose attr", { + tree <- structure( + list( + nodes = list( + list( + name = "base.mod", + model = list( + based_on = list(), + description = "Base population PK model", + tags = list("base") + ), + run = list(start = list( + model_hashes = list(blake3 = "f873a13ca1b2c3d4e5f6"), + dataset_hashes = list(blake3 = "8d8189cfaabb11223344") + )) + ), + list( + name = "run001.mod", + model = list( + based_on = list("base.mod"), + description = "Adding COV step", + tags = list() + ), + run = NULL + ) + ) + ), + class = "hyperion_nonmem_tree" + ) + attr(tree, "verbose") <- TRUE + expect_snapshot(print(tree)) +}) + +test_that("hyperion_nonmem_tree verbose print renders 6-column table", { + tree <- structure( + list( + nodes = list( + list( + name = "base.mod", + model = list( + based_on = list(), + description = "Base population PK model", + tags = list("base") + ), + run = list(start = list( + model_hashes = list(blake3 = "f873a13ca1b2c3d4e5f6"), + dataset_hashes = list(blake3 = "8d8189cfaabb11223344") + )) + ), + list( + name = "run001.mod", + model = list( + based_on = list("base.mod"), + description = "Adding COV step, unfixing CL", + tags = list("covariates", "unfixed") + ), + run = list(start = list( + model_hashes = list(blake3 = "1a0f07a1112233445566"), + dataset_hashes = list(blake3 = "8d8189cfaabb11223344") + )) + ), + list( + name = "run002.mod", + model = list( + based_on = list("run001.mod"), + description = "Not yet run", + tags = list() + ), + run = NULL + ) + ) + ), + class = "hyperion_nonmem_tree" + ) + expect_snapshot(print(tree, verbose = TRUE)) +}) diff --git a/tests/testthat/test-lineage-helpers.R b/tests/testthat/test-lineage-helpers.R deleted file mode 100644 index 3c6be5af..00000000 --- a/tests/testthat/test-lineage-helpers.R +++ /dev/null @@ -1,151 +0,0 @@ -test_that("get_model_ancestors returns ancestors in order", { - tree <- structure( - list( - nodes = list( - "run001.mod" = list(based_on = list(), description = "Base model"), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Child" - ), - "run003.mod" = list( - based_on = list("run002.mod"), - description = "Grandchild" - ) - ) - ), - class = "hyperion_nonmem_tree" - ) - - # run001 has no ancestors - - expect_equal(get_model_ancestors(tree, "run001"), character(0)) - expect_equal(get_model_ancestors(tree, "run001.mod"), character(0)) - - # run002's ancestor is run001 - expect_equal(get_model_ancestors(tree, "run002"), "run001") - - # run003's ancestors are run002, run001 (parent to root order) - expect_equal(get_model_ancestors(tree, "run003"), c("run002", "run001")) -}) - -test_that("get_model_descendants returns all descendants", { - tree <- structure( - list( - nodes = list( - "run001.mod" = list(based_on = list(), description = "Base model"), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Child 1" - ), - "run003.mod" = list( - based_on = list("run001.mod"), - description = "Child 2" - ), - "run004.mod" = list( - based_on = list("run002.mod"), - description = "Grandchild" - ) - ) - ), - class = "hyperion_nonmem_tree" - ) - - # run001 has three descendants - descendants <- get_model_descendants(tree, "run001") - expect_true(all(c("run002", "run003", "run004") %in% descendants)) - - # run002 has one descendant - expect_equal(get_model_descendants(tree, "run002"), "run004") - - # run003 has no descendants - expect_equal(get_model_descendants(tree, "run003"), character(0)) - - # run004 has no descendants - - expect_equal(get_model_descendants(tree, "run004"), character(0)) -}) - -test_that("are_models_in_lineage detects ancestor-descendant relationships", { - tree <- structure( - list( - nodes = list( - "run001.mod" = list(based_on = list(), description = "Base model"), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Child 1" - ), - "run003.mod" = list( - based_on = list("run001.mod"), - description = "Child 2" - ), - "run004.mod" = list( - based_on = list("run002.mod"), - description = "Grandchild" - ) - ) - ), - class = "hyperion_nonmem_tree" - ) - - # Direct parent-child - - expect_true(are_models_in_lineage(tree, "run001", "run002")) - expect_true(are_models_in_lineage(tree, "run002", "run001")) - - # Grandparent-grandchild - expect_true(are_models_in_lineage(tree, "run001", "run004")) - expect_true(are_models_in_lineage(tree, "run004", "run001")) - - # Siblings are NOT in direct lineage - - expect_false(are_models_in_lineage(tree, "run002", "run003")) - expect_false(are_models_in_lineage(tree, "run003", "run002")) - - # Cousins are NOT in direct lineage - expect_false(are_models_in_lineage(tree, "run003", "run004")) -}) - -test_that("lineage functions handle .mod suffix correctly", { - tree <- structure( - list( - nodes = list( - "run001.mod" = list(based_on = list(), description = "Base"), - "run002.mod" = list( - based_on = list("run001.mod"), - description = "Child" - ) - ) - ), - class = "hyperion_nonmem_tree" - ) - - # With and without .mod suffix should work - expect_equal(get_model_ancestors(tree, "run002"), "run001") - expect_equal(get_model_ancestors(tree, "run002.mod"), "run001") - - expect_true(are_models_in_lineage(tree, "run001", "run002")) - expect_true(are_models_in_lineage(tree, "run001.mod", "run002.mod")) - expect_true(are_models_in_lineage(tree, "run001", "run002.mod")) -}) - -test_that("lineage functions error on invalid input", { - not_a_tree <- list(nodes = list()) - - expect_error(get_model_ancestors(not_a_tree, "run001")) - expect_error(get_model_descendants(not_a_tree, "run001")) - expect_error(are_models_in_lineage(not_a_tree, "run001", "run002")) -}) - -test_that("get_model_ancestors errors on circular lineage", { - tree <- structure( - list( - nodes = list( - "run001.mod" = list(based_on = list("run002.mod")), - "run002.mod" = list(based_on = list("run001.mod")) - ) - ), - class = "hyperion_nonmem_tree" - ) - - expect_error(get_model_ancestors(tree, "run001")) -}) diff --git a/tests/testthat/test-lineage.R b/tests/testthat/test-lineage.R new file mode 100644 index 00000000..887fb355 --- /dev/null +++ b/tests/testthat/test-lineage.R @@ -0,0 +1,27 @@ +test_that("get_model_lineage() returns the whole project tree", { + expect_snapshot(get_model_lineage()) +}) + +test_that("get_model_lineage(model) returns the model's full lineage", { + expect_snapshot(get_model_lineage("extdata/models/onecmt/run003.mod")) +}) + +test_that("get_model_lineage(from, to) slices between two models", { + expect_snapshot( + get_model_lineage( + from = "extdata/models/onecmt/run001.mod", + to = "extdata/models/onecmt/run003b1.mod" + ) + ) +}) + +test_that("lineage helpers return project-relative paths", { + expect_snapshot(get_model_ancestors("extdata/models/onecmt/run003b1.mod")) + expect_snapshot(get_model_descendants("extdata/models/onecmt/run001.mod")) + expect_snapshot( + are_models_in_lineage( + "extdata/models/onecmt/run001.mod", + "extdata/models/onecmt/run003b1.mod" + ) + ) +}) diff --git a/tests/testthat/test-model-methods-edge.R b/tests/testthat/test-model-methods-edge.R index f609bbba..6408a5f5 100644 --- a/tests/testthat/test-model-methods-edge.R +++ b/tests/testthat/test-model-methods-edge.R @@ -15,7 +15,7 @@ test_that("format_ignore_condition falls back for unknown operators", { ValueFilter = list( field = "AN01FL", op = "Between", - value = "0" + value = list(Number = 0) ) ) @@ -23,7 +23,7 @@ test_that("format_ignore_condition falls back for unknown operators", { }) test_that("get_theta_parameter_data returns NULL with no parameters", { - x <- list(theta_parameters = list()) + x <- list(thetas = list()) expect_null(get_theta_parameter_data( x, digits = NULL, @@ -50,22 +50,11 @@ test_that("get_random_effect_parameter_data handles BlockSame copying", { blocks <- list( list( structure = list(Block = list(size = 2)), - parametrization = "LogNormal", + fixed = TRUE, + parametrization = "Cholesky", parameters = list( - list( - initial_value = 0.1, - lower_bound = 0, - upper_bound = 1, - is_fixed = FALSE, - comment = "A" - ), - list( - initial_value = 0.2, - lower_bound = 0, - upper_bound = 1, - is_fixed = TRUE, - comment = "B" - ) + list(value = 0.1, comment = "A"), + list(value = 0.2, comment = "B") ) ), list(structure = list(BlockSame = list(size = 2))) diff --git a/tests/testthat/test-name-lookup-omega.R b/tests/testthat/test-name-lookup-omega.R deleted file mode 100644 index ab55ed86..00000000 --- a/tests/testthat/test-name-lookup-omega.R +++ /dev/null @@ -1,27 +0,0 @@ -test_that("format_omega_display_name does not duplicate associated theta names", { - # Theta partially in name - only appends missing theta - expect_equal( - format_omega_display_name("Corr-CL", c("CL", "V")), - "Corr-CL V" - ) - - # Both thetas already in name - no duplication - expect_equal( - format_omega_display_name("Corr-CL-V", c("CL", "V")), - "Corr-CL-V" - ) -}) - -test_that("format_omega_display_name appends associated theta when not present", { - # Single theta not in name - expect_equal( - format_omega_display_name("IIV", "TVCL"), - "IIV TVCL" - ) - - # Multiple thetas not in name - expect_equal( - format_omega_display_name("IIV", c("TVCL", "TVV")), - "IIV TVCL, TVV" - ) -}) diff --git a/tests/testthat/test-parse_raw_omega_comment.R b/tests/testthat/test-parse_raw_omega_comment.R deleted file mode 100644 index 15c0cefd..00000000 --- a/tests/testthat/test-parse_raw_omega_comment.R +++ /dev/null @@ -1,86 +0,0 @@ -test_that("parse omega comments extracts name and associated_theta separately", { - # Name is prefix only, associated_theta stored separately - om_comment <- parse_raw_omega_comment("OMEGA(2,1)", NULL, "OM2,1 CL-VC") - expect_equal(om_comment@nonmem_name, "OMEGA(2,1)") - expect_equal(om_comment@name, "OM2,1") - expect_equal(om_comment@parameterization, NULL) - expect_equal(om_comment@associated_theta, c("CL", "VC")) - - om_comment <- parse_raw_omega_comment("OMEGA(2,1)", NULL, "OM2,1 CL-VC ;log") - expect_equal(om_comment@nonmem_name, "OMEGA(2,1)") - expect_equal(om_comment@name, "OM2,1") - expect_equal(om_comment@parameterization, "LogNormal") - expect_equal(om_comment@associated_theta, c("CL", "VC")) -}) - -test_that("omega name is prefix only, associated_theta stored separately", { - # Already hyphenated - extracts prefix and theta - result <- extract_raw_omega_parts("IIV-CL") - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, "CL") - - # Space between prefix and theta - result <- extract_raw_omega_parts("IIV CL") - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, "CL") - - # Linking word "on" - skipped - result <- extract_raw_omega_parts("IIV on CL") - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, "CL") - - # Different prefix - result <- extract_raw_omega_parts("OM1 CL") - expect_equal(result$name, "OM1") - expect_equal(result$associated_theta, "CL") - - # Another linking word - result <- extract_raw_omega_parts("eta on V") - expect_equal(result$name, "eta") - expect_equal(result$associated_theta, "V") - - # Correlation with hyphenated thetas - result <- extract_raw_omega_parts("Corr CL-V") - expect_equal(result$name, "Corr") - expect_equal(result$associated_theta, c("CL", "V")) -}) - -test_that("associated_theta splits unless matches known theta", { - # Without context - splits on separators - result <- extract_raw_omega_parts("IIV on CL/F") - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, c("CL", "F")) - - result <- extract_raw_omega_parts("Corr CL/V") - expect_equal(result$name, "Corr") - expect_equal(result$associated_theta, c("CL", "V")) - - # With known_thetas context - preserves known names - result <- extract_raw_omega_parts( - "IIV on CL/F", - known_thetas = c("CL/F", "V") - ) - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, "CL/F") - - # Case-insensitive match, preserve original case - result <- extract_raw_omega_parts("IIV on cl/f", known_thetas = c("CL/F")) - expect_equal(result$name, "IIV") - expect_equal(result$associated_theta, "cl/f") - - # With context but no match - still splits - result <- extract_raw_omega_parts("Corr CL/V", known_thetas = c("CL/F", "KA")) - expect_equal(result$name, "Corr") - expect_equal(result$associated_theta, c("CL", "V")) -}) - -test_that("linking words are skipped", { - result <- extract_raw_omega_parts("IIV on CL") - expect_equal(result$associated_theta, "CL") - - result <- extract_raw_omega_parts("IIV for V") - expect_equal(result$associated_theta, "V") - - result <- extract_raw_omega_parts("eta of KA") - expect_equal(result$associated_theta, "KA") -}) diff --git a/tests/testthat/test-parse_raw_theta_comment.R b/tests/testthat/test-parse_raw_theta_comment.R deleted file mode 100644 index f96b77b2..00000000 --- a/tests/testthat/test-parse_raw_theta_comment.R +++ /dev/null @@ -1,11 +0,0 @@ -test_that("parse theta comments works", { - th <- parse_raw_theta_comment("THETA1", NULL, "THETA1 RUV :ADD") - expect_equal(th@nonmem_name, "THETA1") - expect_equal(th@name, "RUV") - expect_equal(th@parameterization, "AddErr") - - th <- parse_raw_theta_comment("THETA2", NULL, "THETA2 CL :log") - expect_equal(th@nonmem_name, "THETA2") - expect_equal(th@name, "CL") - expect_equal(th@parameterization, "LogNormal") -}) diff --git a/tests/testthat/test-split-theta-reference.R b/tests/testthat/test-split-theta-reference.R deleted file mode 100644 index 71e84339..00000000 --- a/tests/testthat/test-split-theta-reference.R +++ /dev/null @@ -1,7 +0,0 @@ -test_that("split_theta_reference trims whitespace around separators", { - result <- split_theta_reference("CL / V") - expect_equal(result, c("CL", "V")) - - result <- split_theta_reference("CL, V") - expect_equal(result, c("CL", "V")) -}) diff --git a/tests/testthat/test-split_theta_reference.R b/tests/testthat/test-split_theta_reference.R deleted file mode 100644 index af10d6cc..00000000 --- a/tests/testthat/test-split_theta_reference.R +++ /dev/null @@ -1,15 +0,0 @@ -test_that("split_theta_reference respects known thetas", { - # No known thetas - splits - expect_equal(split_theta_reference("CL/F"), c("CL", "F")) - expect_equal(split_theta_reference("CL-V"), c("CL", "V")) - expect_equal(split_theta_reference("CL"), "CL") - - # Known theta - keeps as-is - expect_equal(split_theta_reference("CL/F", c("CL/F", "V")), "CL/F") - - # Case-insensitive match - expect_equal(split_theta_reference("cl/f", c("CL/F")), "cl/f") - - # No match in known - splits - expect_equal(split_theta_reference("CL/V", c("CL/F", "KA")), c("CL", "V")) -}) diff --git a/tests/testthat/test-strip-param-prefix-parens.R b/tests/testthat/test-strip-param-prefix-parens.R deleted file mode 100644 index cfdf6cb9..00000000 --- a/tests/testthat/test-strip-param-prefix-parens.R +++ /dev/null @@ -1,9 +0,0 @@ -test_that("parameter prefixes with parentheses are stripped", { - theta_parts <- extract_raw_theta_parts("THETA(1): CL (L/h)") - expect_equal(theta_parts$name, "CL") - expect_equal(theta_parts$unit, "L/h") - - sigma_parts <- extract_raw_sigma_parts("SIGMA(1): PropErr ; prop") - expect_equal(sigma_parts$name, "PropErr") - expect_equal(sigma_parts$parameterization, "prop") -}) diff --git a/tests/testthat/test-strip-param-prefix.R b/tests/testthat/test-strip-param-prefix.R deleted file mode 100644 index 1f53a8ec..00000000 --- a/tests/testthat/test-strip-param-prefix.R +++ /dev/null @@ -1,9 +0,0 @@ -test_that("lowercase parameter prefixes are stripped", { - theta_parts <- extract_raw_theta_parts("theta1: CL (L/h)") - expect_equal(theta_parts$name, "CL") - expect_equal(theta_parts$unit, "L/h") - - omega_parts <- extract_raw_omega_parts("omega1: IIV CL ; exp") - expect_equal(omega_parts$name, "IIV") - expect_equal(omega_parts$associated_theta, "CL") -}) diff --git a/vignettes/ext.Rmd b/vignettes/ext.Rmd index e67f0e99..1f55a81c 100644 --- a/vignettes/ext.Rmd +++ b/vignettes/ext.Rmd @@ -12,6 +12,7 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) +knitr::opts_knit$set(root.dir = system.file("extdata", package = "hyperion")) ``` ```{r setup} @@ -19,24 +20,22 @@ library(ggplot2) library(dplyr) library(tidyr) library(hyperion) - -test_data_dir <- system.file("extdata", package = "hyperion") ``` ## get parameter estimates ```{r} -get_parameters(file.path(test_data_dir, "models", "onecmt", "run002")) +get_parameters(file.path("models", "onecmt", "run002")) -get_parameters(file.path(test_data_dir, "models", "onecmt", "run002.mod")) +get_parameters(file.path("models", "onecmt", "run002.mod")) -get_parameters(file.path(test_data_dir, "models", "onecmt", "run002", "run002.ext")) +get_parameters(file.path("models", "onecmt", "run002", "run002.ext")) -get_parameters(file.path(test_data_dir, "models", "onecmt", "run002_metadata.json")) +get_parameters(file.path("models", "onecmt", "run002_metadata.json")) ``` ```{r} get_parameters( - file.path(test_data_dir, "models", "onecmt", "run002") + file.path("models", "onecmt", "run002") ) |> mutate( `95% CI` = paste0( @@ -50,7 +49,7 @@ get_parameters( ```{r} untransformed_df <- get_parameters( - file.path(test_data_dir, "models", "onecmt", "run002") + file.path("models", "onecmt", "run002") ) transformed_df <- untransformed_df |> @@ -76,7 +75,7 @@ transformed_df ## Read ext file ```{r} read_ext_file( - file.path(test_data_dir, "ext", "bql.ext"), + file.path("ext", "bql.ext"), line_prefixes = "-1000000000", parameters_only = TRUE ) @@ -84,7 +83,7 @@ read_ext_file( ```{r} -read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its") |> +read_ext_file(file.path("ext", "itsimp.ext"), only_method = "its") |> filter(iteration > 0) |> pivot_longer( cols = starts_with("THETA"), @@ -103,7 +102,7 @@ read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its" ``` ```{r} -read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its") |> +read_ext_file(file.path("ext", "itsimp.ext"), only_method = "its") |> filter(iteration > 0) |> pivot_longer( cols = starts_with("OMEGA"), @@ -122,7 +121,7 @@ read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its" ``` ```{r} -read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its") |> +read_ext_file(file.path("ext", "itsimp.ext"), only_method = "its") |> filter(iteration > 0) |> pivot_longer( cols = starts_with("SIGMA"), @@ -142,7 +141,7 @@ read_ext_file(file.path(test_data_dir, "ext", "itsimp.ext"), only_method = "its" ```{r} -read_ext_file(file.path(test_data_dir, "ext", "example6.txt.ext")) |> +read_ext_file(file.path("ext", "example6.txt.ext")) |> filter(iteration == -1000000000) ``` diff --git a/vignettes/grd.Rmd b/vignettes/grd.Rmd index ddb863cc..b1e51814 100644 --- a/vignettes/grd.Rmd +++ b/vignettes/grd.Rmd @@ -12,17 +12,16 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) +knitr::opts_knit$set(root.dir = system.file("extdata", package = "hyperion")) ``` ```{r setup} library(hyperion) library(dplyr) - -test_data_dir <- system.file("extdata", package = "hyperion") ``` ```{r} -get_gradients(file.path(test_data_dir, "grd", "bql.grd")) |> +get_gradients(file.path("grd", "bql.grd")) |> slice_tail(n = 1) |> summarize(any_zero = any(across(everything(), ~ .x == 0))) |> pull(any_zero) |> @@ -30,20 +29,20 @@ get_gradients(file.path(test_data_dir, "grd", "bql.grd")) |> ``` ```{r} -get_gradients(file.path(test_data_dir, "models", "onecmt", "run001")) +get_gradients(file.path("models", "onecmt", "run001")) -get_gradients(file.path(test_data_dir, "models", "onecmt", "run001")) +get_gradients(file.path("models", "onecmt", "run001")) -get_gradients(file.path(test_data_dir, "models", "onecmt", "run001", "run001.grd")) +get_gradients(file.path("models", "onecmt", "run001", "run001.grd")) ``` ```{r} -get_gradients(file.path(test_data_dir, "models", "onecmt", "run002")) +get_gradients(file.path("models", "onecmt", "run002")) -get_gradients(file.path(test_data_dir, "models", "onecmt", "run002.mod")) +get_gradients(file.path("models", "onecmt", "run002.mod")) -get_gradients(file.path(test_data_dir, "models", "onecmt", "run002", "run002.grd")) +get_gradients(file.path("models", "onecmt", "run002", "run002.grd")) -get_gradients(file.path(test_data_dir, "models", "onecmt", "run002_metadata.json")) +get_gradients(file.path("models", "onecmt", "run002_metadata.json")) ``` diff --git a/vignettes/hyperion_model.Rmd b/vignettes/hyperion_model.Rmd index e1228c34..49dd1fb2 100644 --- a/vignettes/hyperion_model.Rmd +++ b/vignettes/hyperion_model.Rmd @@ -12,27 +12,38 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) + +vignette_root <- tempfile("hyperion-vignette-") +dir.create(vignette_root, recursive = TRUE) +file.copy( + list.files(system.file("extdata", package = "hyperion"), full.names = TRUE), + vignette_root, + recursive = TRUE +) +file.copy( + system.file("pharos.toml", package = "hyperion"), + vignette_root +) +knitr::opts_knit$set(root.dir = vignette_root) ``` ```{r setup} library(hyperion) - -test_data_dir <- system.file("extdata", package = "hyperion") ``` # Hyperion Model object ```{r} -mod <- read_model(file.path(test_data_dir, "mod", "1001.mod")) +mod <- read_model(file.path("mod", "1001.mod")) mod -mod <- read_model(file.path(test_data_dir, "models", "onecmt", "run002b001.mod")) +mod <- read_model(file.path("models", "onecmt", "run002b001.mod")) mod -mod_nm <- read_model(file.path(test_data_dir, "mod", "nmexample.mod")) +mod_nm <- read_model(file.path("mod", "nmexample.mod")) mod_nm -mod_e <- read_model(file.path(test_data_dir, "mod", "everything.mod")) +mod_e <- read_model(file.path("mod", "everything.mod")) mod_e ``` @@ -43,19 +54,19 @@ attributes(mod) |> names() ``` ```{r} -read_model(file.path(test_data_dir, "models", "onecmt", "run001.mod")) |> - check_dataset() +read_model(file.path("models", "onecmt", "run001.mod")) |> + check_dataset() -read_model(file.path(test_data_dir, "models", "onecmt", "run002.mod")) |> +read_model(file.path("models", "onecmt", "run002.mod")) |> check_dataset() -read_model(file.path(test_data_dir, "models", "onecmt", "run003.mod")) |> +read_model(file.path("models", "onecmt", "run003.mod")) |> check_dataset() ``` ```{r} -read_model(file.path(test_data_dir, "models", "onecmt", "run001.mod")) |> +read_model(file.path("models", "onecmt", "run001.mod")) |> check_model() ``` @@ -63,7 +74,7 @@ read_model(file.path(test_data_dir, "models", "onecmt", "run001.mod")) |> ## model summary can be generated from model object ```{r} -mod <- read_model(file.path(test_data_dir, "models", "onecmt", "run003.mod")) +mod <- read_model(file.path("models", "onecmt", "run003.mod")) mod |> summary() ``` @@ -88,8 +99,8 @@ mod |> get_model_parameter_info() |> audit_parameter_info() # Copy model ```{r} copy_model( - from = file.path(test_data_dir, "models", "onecmt", "run003.mod"), - to = file.path(test_data_dir, "models", "onecmt", "run003b2.mod"), #copies run003 to run003b1 with jittered parameters + from = file.path("models", "onecmt", "run003.mod"), + to = file.path("models", "onecmt", "run003b2.mod"), #copies run003 to run003b1 with jittered parameters description = "Updating run003 to 003b1 with jittered params", jitter = 0.1, overwrite = TRUE, @@ -99,11 +110,11 @@ copy_model( ## Copy model accepts hyperion model object ```{r} -mod <- read_model(file.path(test_data_dir, "models", "onecmt", "run003.mod")) +mod <- read_model(file.path("models", "onecmt", "run003.mod")) -mod |> +mod |> copy_model( - to = file.path(test_data_dir, "models", "onecmt", "run003b2.mod"), + to = file.path("models", "onecmt", "run003b2.mod"), update = "all", description = "Updating run003 with mod object", overwrite = TRUE, @@ -112,10 +123,81 @@ mod |> ``` -# Model Lineage +# Managing metadata and lineage + +## Set and read metadata + +`set_metadata_file()` writes a JSON metadata sidecar next to a model. The +`description`, `tags`, `based_on`, and `copied_from` fields are stored +separately so model provenance can be tracked explicitly. ```{r} -example_tree <- get_model_lineage(file.path(test_data_dir, "models", "onecmt")) +set_metadata_file( + file.path("models", "onecmt", "run003.mod"), + description = "Base one-compartment oral absorption model", + tags = c("base", "key"), + based_on = c("run002.mod") +) + +read_model(file.path("models", "onecmt", "run003.mod")) |> + get_model_metadata() +``` + +## Populate metadata at copy time + +`copy_model()` accepts `based_on` and `tags` so the new model's metadata is +populated as part of the copy. + +```{r} +copy_model( + from = file.path("models", "onecmt", "run003.mod"), + to = file.path("models", "onecmt", "run003b2.mod"), + description = "run003 with jittered params, exploring WT on V", + based_on = c("run003.mod"), + tags = c("exploratory", "wt-on-v"), + jitter = 0.1, + overwrite = TRUE, + seed = 804 +) +``` -example_tree +## Clear metadata fields + +`clear_metadata_file()` selectively clears `based_on`, `copied_from`, and/or +`tags`. Fields not selected are preserved. + +```{r} +clear_metadata_file( + file.path("models", "onecmt", "run003b2.mod"), + tags = TRUE +) +``` + +## Lineage queries + +`get_model_lineage()` returns the project lineage tree. With no arguments +it returns every model rooted at the directory containing `pharos.toml`. + +```{r} +get_model_lineage() +``` + +Pass a model to get its full lineage (ancestors and descendants): + +```{r} +get_model_lineage(file.path("models", "onecmt", "run003.mod")) +``` + +Use `from` and `to` to filter the tree downward, upward, or to the slice +between two models: + +```{r} +get_model_lineage(from = file.path("models", "onecmt", "run001.mod")) + +get_model_lineage(to = file.path("models", "onecmt", "run003b1.mod")) + +get_model_lineage( + from = file.path("models", "onecmt", "run001.mod"), + to = file.path("models", "onecmt", "run003b1.mod") +) ``` diff --git a/vignettes/lst.Rmd b/vignettes/lst.Rmd index 7f2a180b..b71ce5bb 100644 --- a/vignettes/lst.Rmd +++ b/vignettes/lst.Rmd @@ -12,19 +12,18 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) +knitr::opts_knit$set(root.dir = system.file("extdata", package = "hyperion")) ``` ```{r setup} library(hyperion) - -test_data_dir <- system.file("extdata", package = "hyperion") ``` # Parse lst ```{r} get_run_info( read_model( - file.path(test_data_dir, "models", "onecmt", "run001.mod") + file.path("models", "onecmt", "run001.mod") ) ) ``` @@ -32,7 +31,7 @@ get_run_info( ```{r} get_run_info( read_model( - file.path(test_data_dir, "models", "onecmt", "run003b1.mod") + file.path("models", "onecmt", "run003b1.mod") ) ) ``` @@ -40,7 +39,7 @@ get_run_info( ```{r} mod_sum <- summary( read_model( - file.path(test_data_dir, "models", "onecmt", "run002.mod") + file.path("models", "onecmt", "run002.mod") ) ) @@ -49,7 +48,7 @@ mod_sum ```{r} mod_sum <- summary( read_model( - file.path(test_data_dir, "models", "onecmt", "run003b1.mod") + file.path("models", "onecmt", "run003b1.mod") ) ) diff --git a/vignettes/pharos.toml b/vignettes/pharos.toml deleted file mode 100644 index f41f9d85..00000000 --- a/vignettes/pharos.toml +++ /dev/null @@ -1,30 +0,0 @@ -[nonmem] -clean_level = 1 -default_version = "nm760" -files_to_copy = [] - -[nonmem.options] -prsame = false -prcompile = false -prdefault = false -tprdefault = false -background = false -nobuild = false -maxlim = 2 - -[nonmem.versions] -nm760 = "/opt/nonmem/nm760" - -[nonmem.parallel] -mpiexec_path = "/opt/homebrew/bin/mpiexec" -enabled = false -num_cpus = 4 -timeout = 2147483647 - -[nonmem.comments] -type = "type1" -error_on_invalid = false - -[nonmem.summary] -high_correlation_threshold = 0.95 -high_condition_threshold = 1000 diff --git a/vignettes/shk.Rmd b/vignettes/shk.Rmd index f024dc03..4486eaae 100644 --- a/vignettes/shk.Rmd +++ b/vignettes/shk.Rmd @@ -12,33 +12,32 @@ knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) +knitr::opts_knit$set(root.dir = system.file("extdata", package = "hyperion")) ``` ```{r setup} library(hyperion) - -test_data_dir <- system.file("extdata", package = "hyperion") ``` ```{r} -get_eta_shrinkage(file.path(test_data_dir, "shk", "3068.shk")) +get_eta_shrinkage(file.path("shk", "3068.shk")) ``` ```{r} -get_eps_shrinkage(file.path(test_data_dir, "shk", "3068.shk")) +get_eps_shrinkage(file.path("shk", "3068.shk")) ``` ```{r} -get_eta_shrinkage(file.path(test_data_dir, "shk", "bql.shk")) +get_eta_shrinkage(file.path("shk", "bql.shk")) ``` ```{r} -get_eps_shrinkage(file.path(test_data_dir, "shk", "bql.shk")) +get_eps_shrinkage(file.path("shk", "bql.shk")) ``` ```{r} -get_eta_shrinkage(file.path(test_data_dir, "shk", "itsimp.shk")) +get_eta_shrinkage(file.path("shk", "itsimp.shk")) ``` ```{r} -get_eps_shrinkage(file.path(test_data_dir, "shk", "itsimp.shk")) +get_eps_shrinkage(file.path("shk", "itsimp.shk")) ```