diff --git a/DESCRIPTION b/DESCRIPTION index cde9cef..4b79da0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,9 +1,14 @@ Package: vmxr Title: VeloMetrix R Client -Version: 0.1.0 +Version: 0.1.1 Authors@R: c( - person("Eric", "Novik", , "eric@generable.com", role = c("aut", "cre")), - person("Generable", role = c("cph", "fnd"))) + person(given = "Eric", family = "Novik", email = "eric@generable.com", role = c("aut", "cre")), + person(given = "Juho", family = "Timonen", role = "ctb"), + person(given = "Generable", role = c("cph", "fnd"))) +Author: Eric Novik [aut, cre], + Juho Timonen [ctb], + Generable [cph, fnd] +Maintainer: Eric Novik Description: A native R client for the VeloMetrix REST API (vmx-api). Wraps the treatment-to-simulation analysis workflow in ergonomic, pipe-friendly verbs that block-and-poll for asynchronous server jobs and return native R objects @@ -36,3 +41,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) Config/roxygen2/version: 8.0.0 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index ca0ea86..6b622aa 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -27,6 +27,7 @@ export(vmx_dataset_files) export(vmx_dataset_tags) export(vmx_datasets) export(vmx_dosing_input) +export(vmx_dosing_input_status) export(vmx_fit_global_estimates) export(vmx_fit_obs_vs_pred) export(vmx_fit_subject_estimates) @@ -39,6 +40,7 @@ export(vmx_model_build_events) export(vmx_model_build_export) export(vmx_model_build_logs) export(vmx_model_build_report) +export(vmx_model_build_report_create) export(vmx_model_build_results) export(vmx_model_build_runs) export(vmx_model_build_status) @@ -46,6 +48,7 @@ export(vmx_model_catalog) export(vmx_model_data) export(vmx_model_describe) export(vmx_model_fit) +export(vmx_model_fit_postprocessor_status) export(vmx_model_fits) export(vmx_modeling_options) export(vmx_nca) @@ -60,8 +63,12 @@ export(vmx_prep_questions) export(vmx_prep_status) export(vmx_sim_cancel) export(vmx_sim_existing_subject) +export(vmx_sim_existing_subject_from_text) export(vmx_sim_hypothetical_subject) +export(vmx_sim_hypothetical_subject_from_text) +export(vmx_sim_jobs) export(vmx_sim_population) +export(vmx_sim_population_from_text) export(vmx_sim_result) export(vmx_sim_status) export(vmx_studies) diff --git a/NEWS.md b/NEWS.md index a1e2df4..63cb0a2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,13 @@ +# vmxr 0.1.1 + +* Send upload `config_yaml` as inline YAML text, matching the current API form + contract. +* Add wrappers for report creation, model-fit postprocessor status, + dosing-input status, fit simulation-job listing, and simulation `from-text` + creation endpoints. +* Fix examples to use valid time bases and clarify that OpenAPI codegen is not + shipped yet. + # vmxr 0.1.0 First broadly functional release: the client now covers the full analysis diff --git a/R/datasets.R b/R/datasets.R index da70390..7843f90 100644 --- a/R/datasets.R +++ b/R/datasets.R @@ -37,7 +37,13 @@ vmx_upload <- function(study, files, parts <- list(treatment_id = tmt_id, study_id = std_id, mode = mode) if (!is.null(config)) { - parts$config_yaml <- curl::form_file(config, type = "application/yaml") + if (!file.exists(config)) { + vmx_abort( + sprintf("Config file not found: %s", config), + class = "vmx_usage_error" + ) + } + parts$config_yaml <- paste(readLines(config, warn = FALSE), collapse = "\n") } # Repeated `files` form field — a list with duplicate names, spliced in. file_parts <- stats::setNames( diff --git a/R/generated/README.md b/R/generated/README.md index 9775e2e..2f28a28 100644 --- a/R/generated/README.md +++ b/R/generated/README.md @@ -1,11 +1,8 @@ -# Generated bindings (do not hand-edit) +# Generated constants -Files in this directory are produced by [`data-raw/codegen.R`](../../data-raw/codegen.R) -from the vendored OpenAPI spec at -[`inst/openapi/openapi.json`](../../inst/openapi/openapi.json). +This directory is reserved for generated constants from +[`data-raw/codegen.R`](../../data-raw/codegen.R) once the vendored OpenAPI +snapshot and contract test are implemented. -Per the design (section 3, path b), codegen emits **enum/type constants only**; -the HTTP calls are hand-written in `R/*.R` and a contract test -(`tests/testthat/test-contract.R`) asserts the hand-written layer has not -drifted from the spec. Do not edit generated files by hand — re-run codegen -instead. +Per the design (section 3, path b), codegen will emit **enum/type constants +only**; the HTTP calls are hand-written in `R/*.R`. diff --git a/R/modeling.R b/R/modeling.R index b4df2d7..ff41a01 100644 --- a/R/modeling.R +++ b/R/modeling.R @@ -171,6 +171,25 @@ vmx_model_build_report <- function(run, client = vmx_client()) { vmx_get(client, paste0("/model-build-runs/", vmx_id(run, "run"), "/report")) } +#' Request build-run report generation +#' +#' `POST /model-build-runs/{run_id}/report` queues HTML report generation. +#' +#' @param run A build-run id or object. +#' @param subject_plot_mode One of `"all"` or `"none"`. +#' @param client A `vmx_client`. +#' @return A list with report status. +#' @export +vmx_model_build_report_create <- function(run, subject_plot_mode = c("all", "none"), + client = vmx_client()) { + subject_plot_mode <- match.arg(subject_plot_mode) + vmx_post( + client, + paste0("/model-build-runs/", vmx_id(run, "run"), "/report"), + list(subject_plot_mode = subject_plot_mode) + ) +} + #' Cancel a build run #' @param run A build-run id or object. #' @param client A `vmx_client`. @@ -232,6 +251,15 @@ vmx_model_fit <- function(id, client = vmx_client()) { new_vmx_resource(data, "vmx_model_fit", "model_fit_id") } +#' Model-fit postprocessor status +#' @param fit A fit id or `vmx_model_fit`. +#' @param client A `vmx_client`. +#' @return A list with postprocessor status. +#' @export +vmx_model_fit_postprocessor_status <- function(fit, client = vmx_client()) { + vmx_get(client, paste0("/model-fits/", vmx_id(fit, "mf"), "/postprocessor-status")) +} + #' Subject-level parameter estimates (tidy, long) #' #' One row per subject x parameter, with the posterior point estimate (`value`) diff --git a/R/simulation.R b/R/simulation.R index 429133a..43fde5c 100644 --- a/R/simulation.R +++ b/R/simulation.R @@ -18,6 +18,16 @@ vmx_dosing_input <- function(fit, dosing_text, scenario_name, new_vmx_resource(data, "vmx_dosing_input", "dosing_input_id") } +#' Dosing-input status +#' @param dosing_input A dosing-input id or `vmx_dosing_input`. +#' @param client A `vmx_client`. +#' @return A `vmx_dosing_input`. +#' @export +vmx_dosing_input_status <- function(dosing_input, client = vmx_client()) { + data <- vmx_get(client, paste0("/simulation-dosing-inputs/", vmx_dosing_input_id(dosing_input))) + new_vmx_resource(data, "vmx_dosing_input", "dosing_input_id") +} + #' Simulate existing (observed) subjects #' #' `POST /model-fits/{mf_id}/existing-subject-simulation-jobs`. @@ -26,6 +36,7 @@ vmx_dosing_input <- function(fit, dosing_text, scenario_name, #' @param dosing_input A dosing-input id or `vmx_dosing_input`. #' @param subjects Subjects to simulate: a data.frame/tibble with #' `gen_subject_uuid` + `subject_name` columns, or a list of such records. +#' @param min_timepoints Optional minimum number of simulated timepoints. #' @param idempotency_key,retried_from Optional create fields. #' @param wait If `TRUE`, block until the job settles. #' @param ... Polling controls forwarded to [vmx_wait()]. @@ -34,16 +45,40 @@ vmx_dosing_input <- function(fit, dosing_text, scenario_name, #' @export vmx_sim_existing_subject <- function(fit, dosing_input, subjects, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client()) { body <- vmx_compact(list( dosing_input_id = vmx_dosing_input_id(dosing_input), subjects = vmx_rows_to_records(subjects, c("gen_subject_uuid", "subject_name")), + min_timepoints = min_timepoints, idempotency_key = idempotency_key, retried_from = retried_from )) vmx_create_sim_job(fit, "existing-subject-simulation-jobs", body, wait, client, ...) } +#' Simulate existing subjects from dosing text +#' +#' `POST /model-fits/{mf_id}/existing-subject-simulation-jobs/from-text`. +#' +#' @inheritParams vmx_sim_existing_subject +#' @param dosing_text The dosing regimen text. +#' @return A `vmx_simulation_job`. +#' @export +vmx_sim_existing_subject_from_text <- function(fit, dosing_text, subjects, + idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, ..., client = vmx_client()) { + body <- vmx_compact(list( + dosing_text = dosing_text, + subjects = vmx_rows_to_records(subjects, c("gen_subject_uuid", "subject_name")), + min_timepoints = min_timepoints, + idempotency_key = idempotency_key, + retried_from = retried_from + )) + vmx_create_sim_job(fit, "existing-subject-simulation-jobs/from-text", body, wait, client, ...) +} + #' Simulate hypothetical subjects #' #' `POST /model-fits/{mf_id}/hypothetical-subject-simulation-jobs`. @@ -52,6 +87,7 @@ vmx_sim_existing_subject <- function(fit, dosing_input, subjects, #' @param dosing_input A dosing-input id or `vmx_dosing_input`. #' @param subjects A data.frame/tibble with a `subject_name` column plus one #' column per covariate, or a list of `{subject_name, covariates}` records. +#' @param min_timepoints Optional minimum number of simulated timepoints. #' @param idempotency_key,retried_from Optional create fields. #' @param wait If `TRUE`, block until the job settles. #' @param ... Polling controls forwarded to [vmx_wait()]. @@ -60,16 +96,40 @@ vmx_sim_existing_subject <- function(fit, dosing_input, subjects, #' @export vmx_sim_hypothetical_subject <- function(fit, dosing_input, subjects, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client()) { body <- vmx_compact(list( dosing_input_id = vmx_dosing_input_id(dosing_input), subjects = vmx_hypothetical_records(subjects), + min_timepoints = min_timepoints, idempotency_key = idempotency_key, retried_from = retried_from )) vmx_create_sim_job(fit, "hypothetical-subject-simulation-jobs", body, wait, client, ...) } +#' Simulate hypothetical subjects from dosing text +#' +#' `POST /model-fits/{mf_id}/hypothetical-subject-simulation-jobs/from-text`. +#' +#' @inheritParams vmx_sim_hypothetical_subject +#' @param dosing_text The dosing regimen text. +#' @return A `vmx_simulation_job`. +#' @export +vmx_sim_hypothetical_subject_from_text <- function(fit, dosing_text, subjects, + idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, ..., client = vmx_client()) { + body <- vmx_compact(list( + dosing_text = dosing_text, + subjects = vmx_hypothetical_records(subjects), + min_timepoints = min_timepoints, + idempotency_key = idempotency_key, + retried_from = retried_from + )) + vmx_create_sim_job(fit, "hypothetical-subject-simulation-jobs/from-text", body, wait, client, ...) +} + #' Simulate a population scenario #' #' `POST /model-fits/{mf_id}/population-simulation-jobs`. @@ -77,6 +137,7 @@ vmx_sim_hypothetical_subject <- function(fit, dosing_input, subjects, #' @param fit A fit id or `vmx_model_fit`. #' @param dosing_input A dosing-input id or `vmx_dosing_input`. #' @param scenario_name The population scenario name. +#' @param min_timepoints Optional minimum number of simulated timepoints. #' @param idempotency_key,retried_from Optional create fields. #' @param wait If `TRUE`, block until the job settles. #' @param ... Polling controls forwarded to [vmx_wait()]. @@ -85,16 +146,49 @@ vmx_sim_hypothetical_subject <- function(fit, dosing_input, subjects, #' @export vmx_sim_population <- function(fit, dosing_input, scenario_name, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client()) { body <- vmx_compact(list( dosing_input_id = vmx_dosing_input_id(dosing_input), scenario_name = scenario_name, + min_timepoints = min_timepoints, idempotency_key = idempotency_key, retried_from = retried_from )) vmx_create_sim_job(fit, "population-simulation-jobs", body, wait, client, ...) } +#' Simulate a population scenario from dosing text +#' +#' `POST /model-fits/{mf_id}/population-simulation-jobs/from-text`. +#' +#' @inheritParams vmx_sim_population +#' @param dosing_text The dosing regimen text. +#' @return A `vmx_simulation_job`. +#' @export +vmx_sim_population_from_text <- function(fit, dosing_text, scenario_name, + idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, ..., client = vmx_client()) { + body <- vmx_compact(list( + dosing_text = dosing_text, + scenario_name = scenario_name, + min_timepoints = min_timepoints, + idempotency_key = idempotency_key, + retried_from = retried_from + )) + vmx_create_sim_job(fit, "population-simulation-jobs/from-text", body, wait, client, ...) +} + +#' List simulation jobs for a model fit +#' @param fit A fit id or `vmx_model_fit`. +#' @param client A `vmx_client`. +#' @return A tibble. +#' @export +vmx_sim_jobs <- function(fit, client = vmx_client()) { + vmx_items_to_tibble(vmx_paginate(client, paste0("/model-fits/", vmx_id(fit, "mf", "fit"), "/simulation-jobs"))) +} + #' Simulation job status #' @param job A job id (`simjob_...`) or `vmx_simulation_job`. #' @param client A `vmx_client`. diff --git a/README.md b/README.md index 65f792b..d52ebb9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ modeling → simulation workflow in ergonomic, pipe-friendly verbs that Our users are pharmacometricians who work in R/RStudio; `vmxr` keeps the whole analysis next to their data instead of shuttling files and IDs through a shell. -> **Status: functional (v0.1.0), pre-CRAN.** The client covers the full analysis +> **Status: functional (v0.1.1), pre-CRAN.** The client covers the full analysis > workflow end to end — treatments, studies, datasets & prep, data-versions, > modeling-data tables, NCA, modeling (build runs, fits, estimates), simulation, > and the study analysis log — validated against the live API on staging. @@ -61,18 +61,16 @@ tmt <- vmx_treatment_create("Compound XYZ", indication = "atrial fibrillation" study <- vmx_study_create(tmt, "Phase 1 SAD", phase = "1") ds <- vmx_upload(study, c("conc.csv", "dosing.csv"), mode = "initial", wait = TRUE) dv <- vmx_data_version(vmx_prep_status(ds)$data_version_id) -nca <- vmx_nca(dv, time_basis = "actual") +nca <- vmx_nca(dv, time_basis = "observed") vmx_nca_result(nca) ``` ## Design -The package has two layers (see the design doc): - -1. **Low-level bindings** (`R/generated/`) — one function per OpenAPI operation, - generated from a vendored `openapi.json` snapshot. Users rarely call these. -2. **Ergonomic layer** (`R/*.R`) — the curated, hand-written public API that adds - polling, pagination, multipart upload, and tibble/S3 conversion. +The package exposes a curated, hand-written public API in `R/*.R` that adds +polling, pagination, multipart upload, and tibble/S3 conversion. The OpenAPI +snapshot/codegen path is still a development task, not a shipped generated +binding layer. The client holds **no business logic**: the API is the single source of truth. diff --git a/data-raw/codegen.R b/data-raw/codegen.R index 50efacd..d5a5dea 100644 --- a/data-raw/codegen.R +++ b/data-raw/codegen.R @@ -9,8 +9,8 @@ # Steps: # 1. Download the published spec to inst/openapi/openapi.json. # 2. Emit enum/type constants into R/generated/. -# 3. Leave the hand-written calls in R/*.R untouched; the contract test -# (tests/testthat/test-contract.R) verifies they still match the spec. +# 3. Leave the hand-written calls in R/*.R untouched. A future contract test +# should verify that they still match the spec. # Published artifact (see inst/openapi/README.md). OPENAPI_URL <- Sys.getenv( diff --git a/docs/r-client-design.md b/docs/r-client-design.md index df80e65..7cd52ab 100644 --- a/docs/r-client-design.md +++ b/docs/r-client-design.md @@ -346,7 +346,8 @@ vmx_pk(dv, analyte = NULL, format = c("tidy", "nonmem"), blq = c("flag", "drop", "loq_half", "m3"), units = c("as_reported", "si"), - time_basis = c("actual", "nominal"), client = vmx_client()) # -> tibble + time_basis = c("observed", "nominal", "nominal_from_observed_dose"), + client = vmx_client()) # -> tibble vmx_pd(dv, marker = NULL, format = c("tidy", "nonmem"), client = vmx_client()) # -> tibble ``` @@ -381,8 +382,8 @@ gets wrong. Build it once, correctly. - **Dosing fidelity from the server, not inferred:** `RATE` (zero-order/infusion duration), `II`/`ADDL`/`SS` (steady state), route/compartment mapping. - **Units + time basis first-class.** Explicit units (`as_reported` vs `si`) and - `time_basis` (`actual`/`nominal`) — a wrong assumption here silently corrupts - every fit. + `time_basis` (`observed`/`nominal`/`nominal_from_observed_dose`) — a wrong + assumption here silently corrupts every fit. - **Versioned + reproducible.** Everything keys off the immutable `dv_…` id; the content hash is returned in `$meta` so a modeling dataset is citable/repro for regulatory use. @@ -495,7 +496,7 @@ if (vmx_prep_status(ds)$state == "awaiting_input") { } dv <- vmx_data_version(vmx_prep_status(ds)$data_version_id) -nca <- vmx_nca(dv, time_basis = "actual") # creates + waits +nca <- vmx_nca(dv, time_basis = "observed") # creates + waits library(ggplot2) vmx_nca_result(nca) |> @@ -515,7 +516,7 @@ vmx_treatment("tmt_01KT5QSYVYCR9HCBVCKXBVCT8X") |> **Fit a model and pull diagnostics as tibbles:** ```r -run <- vmx_model_build(dv, time_basis = "actual", +run <- vmx_model_build(dv, time_basis = "observed", pd_marker = "GEN_abc123:decreasing", wait = TRUE) fit <- vmx_model_fits(run = run) |> dplyr::slice(1) |> vmx_model_fit() @@ -568,7 +569,7 @@ Deliberately lean — no `tidyverse` hard dependency; works in a vanilla R insta [`vmx_api.openapi`](../services/api/src/vmx_api/openapi.py)). 2. A `clients/r/data-raw/codegen.R` step vendors that spec into `inst/openapi/openapi.json` and (path b) generates enum/type constants only. -3. A **contract test** (`tests/testthat/test-contract.R`) asserts every +3. Add a **contract test** (`tests/testthat/test-contract.R`) asserting every `operationId` the ergonomic layer relies on still exists in the spec and that no wrapped parameter/enum value vanished. This is the cheap insurance that the hand-written layer can't silently drift from the API. diff --git a/inst/openapi/README.md b/inst/openapi/README.md index f658f8e..8e924ff 100644 --- a/inst/openapi/README.md +++ b/inst/openapi/README.md @@ -1,9 +1,9 @@ # Vendored OpenAPI spec -`openapi.json` is a pinned snapshot of the vmx-api OpenAPI document that the -bindings were generated/validated against. The API publishes it as a public GCS -artifact (`corewide-test-public-artifacts/vmx-api/openapi-latest.json`); the -same artifact the frontend codegens from. +`openapi.json` is intended to be a pinned snapshot of the vmx-api OpenAPI +document that the wrappers are validated against. The API publishes it as a +public GCS artifact (`corewide-test-public-artifacts/vmx-api/openapi-latest.json`); +the same artifact the frontend codegens from. -Refresh it with [`data-raw/codegen.R`](../../data-raw/codegen.R). The snapshot is -committed so the contract test is reproducible offline. Targets API 0.2.x. +The snapshot and contract test are not shipped yet. When added, refresh them +with [`data-raw/codegen.R`](../../data-raw/codegen.R). Targets API 0.2.x. diff --git a/man/reexports.Rd b/man/reexports.Rd index d77c622..53eec5d 100644 --- a/man/reexports.Rd +++ b/man/reexports.Rd @@ -11,6 +11,6 @@ These objects are imported from other packages. Follow the links below to see their documentation. \describe{ - \item{tibble}{\code{\link[tibble:as_tibble]{as_tibble()}}} + \item{tibble}{\code{\link[tibble]{as_tibble}}} }} diff --git a/man/vmx_dosing_input_status.Rd b/man/vmx_dosing_input_status.Rd new file mode 100644 index 0000000..45670f0 --- /dev/null +++ b/man/vmx_dosing_input_status.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulation.R +\name{vmx_dosing_input_status} +\alias{vmx_dosing_input_status} +\title{Dosing-input status} +\usage{ +vmx_dosing_input_status(dosing_input, client = vmx_client()) +} +\arguments{ +\item{dosing_input}{A dosing-input id or \code{vmx_dosing_input}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A \code{vmx_dosing_input}. +} +\description{ +Dosing-input status +} diff --git a/man/vmx_model_build_report_create.Rd b/man/vmx_model_build_report_create.Rd new file mode 100644 index 0000000..ea22b65 --- /dev/null +++ b/man/vmx_model_build_report_create.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modeling.R +\name{vmx_model_build_report_create} +\alias{vmx_model_build_report_create} +\title{Request build-run report generation} +\usage{ +vmx_model_build_report_create( + run, + subject_plot_mode = c("all", "none"), + client = vmx_client() +) +} +\arguments{ +\item{run}{A build-run id or object.} + +\item{subject_plot_mode}{One of \code{"all"} or \code{"none"}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A list with report status. +} +\description{ +\code{POST /model-build-runs/{run_id}/report} queues HTML report generation. +} diff --git a/man/vmx_model_fit_postprocessor_status.Rd b/man/vmx_model_fit_postprocessor_status.Rd new file mode 100644 index 0000000..758c751 --- /dev/null +++ b/man/vmx_model_fit_postprocessor_status.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modeling.R +\name{vmx_model_fit_postprocessor_status} +\alias{vmx_model_fit_postprocessor_status} +\title{Model-fit postprocessor status} +\usage{ +vmx_model_fit_postprocessor_status(fit, client = vmx_client()) +} +\arguments{ +\item{fit}{A fit id or \code{vmx_model_fit}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A list with postprocessor status. +} +\description{ +Model-fit postprocessor status +} diff --git a/man/vmx_sim_existing_subject.Rd b/man/vmx_sim_existing_subject.Rd index bcabe17..7e3b4b4 100644 --- a/man/vmx_sim_existing_subject.Rd +++ b/man/vmx_sim_existing_subject.Rd @@ -10,6 +10,7 @@ vmx_sim_existing_subject( subjects, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client() @@ -25,6 +26,8 @@ vmx_sim_existing_subject( \item{idempotency_key, retried_from}{Optional create fields.} +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + \item{wait}{If \code{TRUE}, block until the job settles.} \item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} diff --git a/man/vmx_sim_existing_subject_from_text.Rd b/man/vmx_sim_existing_subject_from_text.Rd new file mode 100644 index 0000000..10427e0 --- /dev/null +++ b/man/vmx_sim_existing_subject_from_text.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulation.R +\name{vmx_sim_existing_subject_from_text} +\alias{vmx_sim_existing_subject_from_text} +\title{Simulate existing subjects from dosing text} +\usage{ +vmx_sim_existing_subject_from_text( + fit, + dosing_text, + subjects, + idempotency_key = NULL, + retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, + ..., + client = vmx_client() +) +} +\arguments{ +\item{fit}{A fit id or \code{vmx_model_fit}.} + +\item{dosing_text}{The dosing regimen text.} + +\item{subjects}{Subjects to simulate: a data.frame/tibble with +\code{gen_subject_uuid} + \code{subject_name} columns, or a list of such records.} + +\item{idempotency_key, retried_from}{Optional create fields.} + +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + +\item{wait}{If \code{TRUE}, block until the job settles.} + +\item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A \code{vmx_simulation_job}. +} +\description{ +\code{POST /model-fits/{mf_id}/existing-subject-simulation-jobs/from-text}. +} diff --git a/man/vmx_sim_hypothetical_subject.Rd b/man/vmx_sim_hypothetical_subject.Rd index 8a43b52..74c0295 100644 --- a/man/vmx_sim_hypothetical_subject.Rd +++ b/man/vmx_sim_hypothetical_subject.Rd @@ -10,6 +10,7 @@ vmx_sim_hypothetical_subject( subjects, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client() @@ -25,6 +26,8 @@ column per covariate, or a list of \verb{\{subject_name, covariates\}} records.} \item{idempotency_key, retried_from}{Optional create fields.} +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + \item{wait}{If \code{TRUE}, block until the job settles.} \item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} diff --git a/man/vmx_sim_hypothetical_subject_from_text.Rd b/man/vmx_sim_hypothetical_subject_from_text.Rd new file mode 100644 index 0000000..af11e3c --- /dev/null +++ b/man/vmx_sim_hypothetical_subject_from_text.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulation.R +\name{vmx_sim_hypothetical_subject_from_text} +\alias{vmx_sim_hypothetical_subject_from_text} +\title{Simulate hypothetical subjects from dosing text} +\usage{ +vmx_sim_hypothetical_subject_from_text( + fit, + dosing_text, + subjects, + idempotency_key = NULL, + retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, + ..., + client = vmx_client() +) +} +\arguments{ +\item{fit}{A fit id or \code{vmx_model_fit}.} + +\item{dosing_text}{The dosing regimen text.} + +\item{subjects}{A data.frame/tibble with a \code{subject_name} column plus one +column per covariate, or a list of \verb{\{subject_name, covariates\}} records.} + +\item{idempotency_key, retried_from}{Optional create fields.} + +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + +\item{wait}{If \code{TRUE}, block until the job settles.} + +\item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A \code{vmx_simulation_job}. +} +\description{ +\code{POST /model-fits/{mf_id}/hypothetical-subject-simulation-jobs/from-text}. +} diff --git a/man/vmx_sim_jobs.Rd b/man/vmx_sim_jobs.Rd new file mode 100644 index 0000000..1378075 --- /dev/null +++ b/man/vmx_sim_jobs.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulation.R +\name{vmx_sim_jobs} +\alias{vmx_sim_jobs} +\title{List simulation jobs for a model fit} +\usage{ +vmx_sim_jobs(fit, client = vmx_client()) +} +\arguments{ +\item{fit}{A fit id or \code{vmx_model_fit}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A tibble. +} +\description{ +List simulation jobs for a model fit +} diff --git a/man/vmx_sim_population.Rd b/man/vmx_sim_population.Rd index acff627..fc18a93 100644 --- a/man/vmx_sim_population.Rd +++ b/man/vmx_sim_population.Rd @@ -10,6 +10,7 @@ vmx_sim_population( scenario_name, idempotency_key = NULL, retried_from = NULL, + min_timepoints = NULL, wait = FALSE, ..., client = vmx_client() @@ -24,6 +25,8 @@ vmx_sim_population( \item{idempotency_key, retried_from}{Optional create fields.} +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + \item{wait}{If \code{TRUE}, block until the job settles.} \item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} diff --git a/man/vmx_sim_population_from_text.Rd b/man/vmx_sim_population_from_text.Rd new file mode 100644 index 0000000..e5ee409 --- /dev/null +++ b/man/vmx_sim_population_from_text.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/simulation.R +\name{vmx_sim_population_from_text} +\alias{vmx_sim_population_from_text} +\title{Simulate a population scenario from dosing text} +\usage{ +vmx_sim_population_from_text( + fit, + dosing_text, + scenario_name, + idempotency_key = NULL, + retried_from = NULL, + min_timepoints = NULL, + wait = FALSE, + ..., + client = vmx_client() +) +} +\arguments{ +\item{fit}{A fit id or \code{vmx_model_fit}.} + +\item{dosing_text}{The dosing regimen text.} + +\item{scenario_name}{The population scenario name.} + +\item{idempotency_key, retried_from}{Optional create fields.} + +\item{min_timepoints}{Optional minimum number of simulated timepoints.} + +\item{wait}{If \code{TRUE}, block until the job settles.} + +\item{...}{Polling controls forwarded to \code{\link[=vmx_wait]{vmx_wait()}}.} + +\item{client}{A \code{vmx_client}.} +} +\value{ +A \code{vmx_simulation_job}. +} +\description{ +\code{POST /model-fits/{mf_id}/population-simulation-jobs/from-text}. +} diff --git a/man/vmxr-package.Rd b/man/vmxr-package.Rd index 31d8ec8..58ad5be 100644 --- a/man/vmxr-package.Rd +++ b/man/vmxr-package.Rd @@ -19,13 +19,9 @@ Useful links: \author{ \strong{Maintainer}: Eric Novik \email{eric@generable.com} -Authors: -\itemize{ - \item Eric Novik \email{eric@generable.com} -} - Other contributors: \itemize{ + \item Juho Timonen [contributor] \item Generable [copyright holder, funder] } diff --git a/tests/testthat/test-datasets-prep.R b/tests/testthat/test-datasets-prep.R index 83fdbb5..631fb26 100644 --- a/tests/testthat/test-datasets-prep.R +++ b/tests/testthat/test-datasets-prep.R @@ -23,6 +23,39 @@ test_that("vmx_datasets lists with filters into a tibble", { expect_match(env$req$url, "study_id=std_1") }) +test_that("vmx_datasets accepts a vmx_study object", { + env <- new.env() + httr2::local_mocked_responses(function(req) { + env$req <- req + httr2::response_json(body = list( + items = list(list(dataset_id = "ds_1", status = "formatted")), next_cursor = NULL + )) + }) + study <- new_vmx_resource(list(study_id = "std_7"), "vmx_study", "study_id") + vmx_datasets(study = study, client = con) + expect_match(env$req$url, "study_id=std_7") +}) + +test_that("vmx_upload sends config_yaml as inline form text", { + data_file <- tempfile(fileext = ".csv") + config_file <- tempfile(fileext = ".yaml") + writeLines("time,conc\n0,1", data_file) + writeLines(c("version: 2", "datasets: []"), config_file) + + env <- capture_one(list(dataset_id = "ds_1", status = "uploaded")) + study <- new_vmx_resource( + list(study_id = "std_1", treatment_id = "tmt_1"), + "vmx_study", + "study_id" + ) + vmx_upload(study, data_file, config = config_file, client = con) + + body <- env$req$body$data + expect_equal(body$study_id, "std_1") + expect_equal(body$treatment_id, "tmt_1") + expect_equal(body$config_yaml, "version: 2\ndatasets: []") +}) + test_that("vmx_dataset fetches and types the resource", { httr2::local_mocked_responses(list(httr2::response_json(body = list( dataset_id = "ds_1", status = "formatted", tags = list(name = "run-A") diff --git a/tests/testthat/test-modeling.R b/tests/testthat/test-modeling.R index fd98364..ecc1e37 100644 --- a/tests/testthat/test-modeling.R +++ b/tests/testthat/test-modeling.R @@ -56,6 +56,14 @@ test_that("vmx_model_build wait=TRUE polls to terminal", { expect_equal(run$status, "succeeded") }) +test_that("vmx_model_build_report_create posts report request", { + env <- capture_one(list(run_id = "run_9", status = "queued", subject_plot_mode = "none")) + out <- vmx_model_build_report_create("run_9", subject_plot_mode = "none", client = con) + expect_equal(out$status, "queued") + expect_equal(env$req$body$data$subject_plot_mode, "none") + expect_match(env$req$url, "/model-build-runs/run_9/report$") +}) + test_that("vmx_wait on a build run raises on failure/cancelled", { httr2::local_mocked_responses(list(httr2::response_json(body = run_item("run_9", "cancelled")))) run <- new_vmx_resource(run_item("run_9"), "vmx_model_build_run", "run_id") @@ -80,6 +88,15 @@ test_that("vmx_model_fits and vmx_model_fit work", { expect_equal(vmx_resource_id(fit), "mf_1") }) +test_that("vmx_model_fit_postprocessor_status calls the current endpoint", { + httr2::local_mocked_responses(list(httr2::response_json(body = list( + model_fit_id = "mf_1", status = "succeeded" + )))) + out <- vmx_model_fit_postprocessor_status("mf_1", client = con) + expect_equal(out$model_fit_id, "mf_1") + expect_equal(out$status, "succeeded") +}) + test_that("vmx_fit_subject_estimates reshapes to tidy long", { httr2::local_mocked_responses(list(httr2::response_json(body = list( model_fit_id = "mf_1", diff --git a/tests/testthat/test-simulation.R b/tests/testthat/test-simulation.R index 8d7cd6d..88018ef 100644 --- a/tests/testthat/test-simulation.R +++ b/tests/testthat/test-simulation.R @@ -24,6 +24,15 @@ test_that("vmx_dosing_input posts text + scenario_names", { expect_match(env$req$url, "/model-fits/mf_1/simulation-dosing-inputs$") }) +test_that("vmx_dosing_input_status fetches dosing input status", { + httr2::local_mocked_responses(list(httr2::response_json(body = list( + dosing_input_id = "di_1", status = "succeeded" + )))) + out <- vmx_dosing_input_status("di_1", client = con) + expect_s3_class(out, "vmx_dosing_input") + expect_equal(out$status, "succeeded") +}) + test_that("vmx_sim_existing_subject builds subject records from a data.frame", { env <- capture_one(job_item("simjob_9")) subj <- data.frame(gen_subject_uuid = c("u1", "u2"), subject_name = c("A", "B")) @@ -35,6 +44,15 @@ test_that("vmx_sim_existing_subject builds subject records from a data.frame", { expect_match(env$req$url, "/existing-subject-simulation-jobs$") }) +test_that("vmx_sim_existing_subject_from_text posts dosing text", { + env <- capture_one(job_item("simjob_9")) + subj <- data.frame(gen_subject_uuid = "u1", subject_name = "A") + vmx_sim_existing_subject_from_text("mf_1", "100 mg qd", subj, min_timepoints = 300, client = con) + expect_equal(env$req$body$data$dosing_text, "100 mg qd") + expect_equal(env$req$body$data$min_timepoints, 300) + expect_match(env$req$url, "/existing-subject-simulation-jobs/from-text$") +}) + test_that("vmx_sim_hypothetical_subject nests covariates", { env <- capture_one(job_item("simjob_9")) subj <- data.frame(subject_name = "H1", WT = 70, AGE = 40) @@ -44,14 +62,40 @@ test_that("vmx_sim_hypothetical_subject nests covariates", { expect_equal(rec$covariates, list(WT = 70, AGE = 40)) }) +test_that("vmx_sim_hypothetical_subject_from_text nests covariates", { + env <- capture_one(job_item("simjob_9")) + subj <- data.frame(subject_name = "H1", WT = 70) + vmx_sim_hypothetical_subject_from_text("mf_1", "100 mg qd", subj, client = con) + expect_equal(env$req$body$data$dosing_text, "100 mg qd") + expect_equal(env$req$body$data$subjects[[1]]$covariates, list(WT = 70)) + expect_match(env$req$url, "/hypothetical-subject-simulation-jobs/from-text$") +}) + test_that("vmx_sim_population posts scenario_name", { env <- capture_one(job_item("simjob_9")) - vmx_sim_population("mf_1", "di_1", "high-dose", client = con) + vmx_sim_population("mf_1", "di_1", "high-dose", min_timepoints = 300, client = con) expect_equal(env$req$body$data$dosing_input_id, "di_1") expect_equal(env$req$body$data$scenario_name, "high-dose") + expect_equal(env$req$body$data$min_timepoints, 300) expect_match(env$req$url, "/population-simulation-jobs$") }) +test_that("vmx_sim_population_from_text posts dosing text", { + env <- capture_one(job_item("simjob_9")) + vmx_sim_population_from_text("mf_1", "100 mg qd", "high-dose", client = con) + expect_equal(env$req$body$data$dosing_text, "100 mg qd") + expect_equal(env$req$body$data$scenario_name, "high-dose") + expect_match(env$req$url, "/population-simulation-jobs/from-text$") +}) + +test_that("vmx_sim_jobs lists jobs for a fit", { + httr2::local_mocked_responses(list(httr2::response_json(body = list( + items = list(job_item("simjob_1", "succeeded")), next_cursor = NULL + )))) + out <- vmx_sim_jobs("mf_1", client = con) + expect_equal(out$simulation_job_id, "simjob_1") +}) + test_that("vmx_sim_status / cancel type the result", { httr2::local_mocked_responses(list(httr2::response_json(body = job_item("simjob_1", "running")))) expect_s3_class(vmx_sim_status("simjob_1", client = con), "vmx_simulation_job") diff --git a/tests/testthat/test-studies-data-versions.R b/tests/testthat/test-studies-data-versions.R index 2dbbaf8..49850d6 100644 --- a/tests/testthat/test-studies-data-versions.R +++ b/tests/testthat/test-studies-data-versions.R @@ -77,6 +77,14 @@ test_that("vmx_data_versions forwards filters as query params", { expect_match(url, "include_archived=false") }) +test_that("vmx_data_versions accepts a vmx_study object", { + cm <- capturing_mock(list(items = list(dv_item("dv_1", study = "std_7")), next_cursor = NULL)) + httr2::local_mocked_responses(cm$mock) + study <- new_vmx_resource(list(study_id = "std_7"), "vmx_study", "study_id") + vmx_data_versions(study = study, client = con) + expect_match(cm$captured$req$url, "study_id=std_7") +}) + test_that("vmx_data_version fetches and types the resource", { cm <- capturing_mock(dv_item("dv_42")) httr2::local_mocked_responses(cm$mock) diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index ab9b5c6..293e0a6 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -7,8 +7,8 @@ vignette: > %\VignetteEncoding{UTF-8} --- -```{r, include = FALSE} -knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) +```{r, include = FALSE, purl = FALSE} +knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE, purl = FALSE) ``` `vmxr` is a native R client for the VeloMetrix API. It collapses the @@ -24,7 +24,7 @@ tibbles and typed S3 objects. `vmxr` uses the same mental model as the CLI: a base URL and an Authentik PAT, read from the environment (or `~/.Renviron`). -```{r} +```{r, purl = FALSE} # ~/.Renviron # VMX_API_BASE_URL=https://vmx-api.staging.gnrbl.co # VMX_API_TOKEN=pat_... @@ -35,7 +35,7 @@ vmx_whoami() # confirms the PAT works ## The whole workflow, in-session -```{r} +```{r, purl = FALSE} tmt <- vmx_treatment_create("Compound XYZ", indication = "atrial fibrillation") study <- vmx_study_create(tmt, "Phase 1 SAD", phase = "1") @@ -43,14 +43,14 @@ ds <- vmx_upload(study, files = c("conc.csv", "dosing.csv"), mode = "initial", wait = TRUE) dv <- vmx_data_version(vmx_prep_status(ds)$data_version_id) -nca <- vmx_nca(dv, time_basis = "actual") # creates + waits +nca <- vmx_nca(dv, time_basis = "observed") # creates + waits vmx_nca_result(nca) ``` ## Model-ready data into R -```{r} +```{r, purl = FALSE} dv <- vmx_data_version("dv_...") # tidy domain tables @@ -64,7 +64,7 @@ md$pk; md$subjects; md$meta ## Fit a model and pull diagnostics -```{r} +```{r, purl = FALSE} run <- vmx_model_build(dv, time_basis = "observed", wait = TRUE) fit <- vmx_model_fits(run = run) |> dplyr::slice(1) |> dplyr::pull(model_fit_id) |> vmx_model_fit()