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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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 <eric@generable.com>
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
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -39,13 +40,15 @@ 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)
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)
Expand All @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 7 additions & 1 deletion R/datasets.R
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 6 additions & 9 deletions R/generated/README.md
Original file line number Diff line number Diff line change
@@ -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`.
28 changes: 28 additions & 0 deletions R/modeling.R
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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`)
Expand Down
94 changes: 94 additions & 0 deletions R/simulation.R
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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()].
Expand All @@ -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`.
Expand All @@ -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()].
Expand All @@ -60,23 +96,48 @@ 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`.
#'
#' @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()].
Expand All @@ -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`.
Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ modeling &rarr; 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.
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions data-raw/codegen.R
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading