vmx_nlmixr_data() is specimen-aware. A marker measured in more than one
specimen is one marker with one input per specimen (dataversion-schema §4.2,
§11.6; api-contract §5.6.2), and the specimen is always the caller's choice:
pk_specimen argument names one recorded PK specimen or
vmx_all_specimens() to pool them. It is required when the PK data records
more than one specimen; without it the call stops with an error that names
the specimens. The DataVersion's recommended specimen is never used
implicitly.pd_markers accepts a named list mapping each marker to specimen names or
vmx_all_specimens(), or vmx_pd_marker() selections as the modeling
requests take them (their direction is ignored). Each selected marker-and-specimen pair is its own
endpoint with its own DVID and compartment; pd_markers = TRUE gives
every eligible pair its own endpoint and never pools. A marker named by a
bare string must be recorded in a single specimen.gen_uuid, specimen_type) rather than by marker uuid alone,
which let one specimen's verdict overwrite another's; one PK specimen by
pk_marker_eligibility; PK All specimens by pk_modeling_eligibility; PD
All specimens by the marker-analysis All-specimens verdict (or, without
marker-analysis metadata, by its recorded pair verdicts). A scoped verdict
that is not on the DataVersion itself is read from
GET /data-versions/{id}/marker-analysis.NA unit.specimen_type column (each observation's own recorded
specimen, NA on dose rows), the endpoints attribute gains
specimen_type and all_specimens, and the "vmx" attribute gains
pk_specimen.A DataVersion that predates specimens (no specimen_type column, verdicts
keyed by marker uuid) is assembled exactly as before, without the new
columns; its "vmx" attribute only gains pk_specimen = NULL.
NCA runs, modeling previews and model builds now state their specimens, as the API requires (api-contract §5.6.2). Breaking change:
vmx_nca(), vmx_modeling_options() and vmx_model_build() take a
required pk_specimen: one specimen type string such as "plasma", or the
new vmx_all_specimens() sentinel to pool every specimen. All specimens is
never a default, and vmxr never applies the data version's recommended
specimen. A call without pk_specimen fails and lists the PK specimens the
data version offers.vmx_pd_marker(gen_uuid, specimen, direction), where specimen is one specimen type or
vmx_all_specimens(). Two specimens of one marker may be separate entries.
The bare-UUID form of vmx_modeling_options(pd_marker =) and the
"uuid:direction" shorthand of vmx_model_build(pd_marker =) are no
longer accepted. vmx_modeling_options() ignores direction, so the same
list can be previewed and then built.marker_scopes
representation, so runs and fits that pool a PD marker across specimens can
be created, read and listed. vmx_fit_obs_vs_pred() PD marker attributes
keep the fitted specimen_type or all_specimens.vmx_nca_marker_selection(), vmx_model_build_marker_selection() and
vmx_model_fit_marker_selection() read the selections the API accepted.invalid_pk_specimen,
invalid_pd_markers, marker_analysis_unavailable,
marker_scopes_representation_required) are raised as
vmx_specimen_error (a vmx_api_error) with a hint on what to change.vmx_data_version_export() called without dest no longer returns the
signed download_url (AGE-240). The server's download link is short-lived —
its window is narrowing to 60 seconds — so handing it back to an interactive
caller invited them to copy a link that would refuse them moments later. The
no-dest path now returns the rest of the envelope (its size, file list and
data version) with download_url removed; the link's presence and shape are
still validated first. To obtain a bundle, pass dest — that path is
unchanged, minting and streaming in one step, and stays correct at any window.
vmx_nlmixr_data() gains an optional replicate argument (AGE-108). With
replicate = TRUE the layout carries a REPLICATE column: the 1-based rank of
each observation within its subject, endpoint and time, NA on dose rows and
1 for an observation with no same-time partner. The index is derived in R;
the DataVersion carries no replicate column. Drop REPLICATE before fitting,
since rxode2 and nlmixr2 treat extra columns as covariates. Behaviour change
for every caller, flag or not: observations that share a subject, endpoint and
time are now emitted in gen_measurement_uuid order, then source-row order,
instead of source order alone. The rows are the same; only their order within
such a group can change. The endpoints attribute also gains
has_repeated_times.
vmx_data_versions(eligible_for_modeling=) now filters correctly against both
the 0.2.2 and 0.3 APIs (AGE-78). The 0.3 API dropped the flat
eligible_for_modeling list filter for the basis-scoped
pk_eligible_for_modeling_after_qc (requiring a time_basis, api-contract
§5.4), and silently ignored the old param — so filtering against staging
returned the full unfiltered list. The client now detects the served contract
from /health's api_contract_version (§5.13): against 0.3 it maps
eligible_for_modeling = TRUE to pk_eligible_for_modeling_after_qc=true
(the same after-QC admission flag vmx_nlmixr_data() defaults to) plus the
new optional time_basis argument; against 0.2.2 it keeps sending
eligible_for_modeling unchanged. A 0.3 server asked to filter without a
time_basis now errors (naming the four accepted bases) rather than returning
an unfiltered list. Note the mapped filters answer subtly different questions:
0.2.2's eligible_for_modeling is basis-agnostic, while 0.3's
pk_eligible_for_modeling_after_qc is scoped to the chosen time_basis, so
the same argument narrows to one basis against a 0.3 server.
Table fetching and vmx_nlmixr_data() work against both the API 0.3 shape
and the API 0.2.2 shape still deployed on some workspaces (AGE-69). A server that
advertises time_bases but ignores the time_basis parameter is no longer
refused: the result records "basis_echoed" = FALSE and the assembler
falls back to the basis-named time column. When the before/after-QC
eligibility pair is absent, the single eligible_for_modeling flag (the
0.2.x per-basis exports) is the admission filter; the applied column is
recorded as attr(x, "vmx")$eligibility_flag. A flat canonical table with no
flag still requires eligibility = "all".
vmx_nlmixr_data() is implemented (AGE-64). It assembles the NONMEM-layout
event dataset nlmixr2 / rxode2 consume — ID, TIME, DV, AMT, EVID,
MDV, CMT, RATE, II/ADDL/SS, CENS/LIMIT, DVID, subject_id
plus wide subject covariates — from a DataVersion's prepared pk, dosing,
subjects, covariates and (optionally) pd tables. Only rows the
DataVersion marks eligible for modeling are admitted (the after-QC flag by
default, the contract's mandatory admission filter; before_qc and all
are available for review). BLQ/ALQ rows are encoded as censored
observations, infusions carry RATE, routes map to compartments through
vmx_nlmixr_default_cmt(), and the "vmx" attribute records the basis,
units, analyte, endpoints and every dropped-row count.
API 0.3 table projection: vmx_data_version_table(), vmx_subjects(),
vmx_pk(), vmx_dosing(), vmx_pd() and vmx_model_data() gain a
time_basis argument. The DataVersion's recommended basis is used by default
(its {value, reason} object or the older bare-string form), an unavailable
basis is refused client-side, and the served time_basis is echoed as an
attribute. vmx_model_data() now also carries $labs, $covariates and
$meta$time_basis.
Collection functions continue to return all matching rows. Cursor pages are followed automatically with strict envelope validation and repeated-cursor detection, so malformed pagination fails instead of truncating results or looping forever.
Estimate helpers now preserve the server-selected point statistic, interval
kind, interval level, and additive tagged metadata. They no longer hardcode
posterior/credible semantics or silently pad malformed parallel arrays.
Observed-vs-predicted helpers consume the current Estimate envelope and
retain PK/PD units and marker identity.
Successful responses are checked for required IDs, array alignment, page
consistency, declared table types, and other contract-critical shape
invariants. Malformed success payloads raise vmx_response_error rather than
being silently recycled, truncated, or attached to the wrong resource.
PK-only modeling previews now send pd_markers = [], matching PK-only model
build submission. Simulation inputs validate canonical simdose_ IDs and
subject records, and dosing inputs can be passed to vmx_wait().
vmx_dosing() exposes the separate dosing-event domain, and
vmx_model_data() includes $dosing while validating required table
availability metadata.
Polling stops immediately on any terminal state, surfaces safe server failure copy, rejects unknown statuses and invalid controls, and uses resource-specific long-running defaults. NCA, simulation, dosing-input, and model-build defaults include a persistence cushion beyond their worker execution ceilings.
Nullable update fields can be explicitly cleared without allowing NULL for
required fields. OIDC caches now reject structurally invalid tokens and fail
loudly if secure atomic replacement does not succeed.
Prep-question tibbles now retain defaults, grouping, resolution hints, referents, rationales, and data previews. Prep answers support idempotency keys, data-version exports require the canonical matching envelope, and create helpers reject malformed upload compositions, retry identifiers, simulation bounds, and ambiguous scalar inputs before dispatch.
Study listing now exposes the API's created_since filter.
vmx_login() device-code prompt now matches the actual flow (GEN-2378).
Because the browser is opened at verification_uri_complete (the URL that
already embeds the user_code), the consent page shows the code pre-filled
and never asks the user to paste it. vmx_login() now prints its own guidance
before the flow — "check the code shown in the browser matches, then approve",
plus a note that you may not be prompted to paste it — reframing the misleading
"copy … and paste when requested" line that upstream httr2 prints. The
guidance shows only in the interactive/browser branch; the non-interactive
fallback (plain verification_uri, where you do enter the code) is left to
httr2's accurate "Visit … and enter code" message.
OIDC access tokens now self-heal on a reused client (GEN-2344). vmx_client()
no longer freezes the OIDC access token at construction: the bearer token is
resolved per request via a provider closure, so a long-lived
con <- vmx_client() silently refreshes from the cached refresh token when the
short-lived access token nears expiry and keeps working across a full session
instead of throwing vmx_auth_error a few minutes in. Also: device-flow
failures (user-denied / code-expired / polling-exhausted) are wrapped as
vmx_auth_error; a cached token whose issuer/client_id do not match the
current config is no longer silently reused or clobbered (interactive login
warns first, non-interactive raises a message naming the mismatch); the
"no usable cached token" message now describes the actual cause; and
VMX_OIDC_TOKEN_CACHE is documented as a testing-only override.
Native OIDC device-code authentication (GEN-2332). vmx_login() runs the
RFC 8628 device-code flow via httr2::oauth_flow_device() against the
Authentik provider (public client, offline_access scope for a refresh
token) and caches the token. vmx_client() now auto-authenticates from
that cache when no VMX_API_TOKEN / token= is set, refreshing silently and
prompting vmx_login() only when there's no usable cached token. The token
is cached as plain JSON at ~/.config/vmx/oidc-token.json — the same path and
shape the vmx CLI uses (0600) — so one login serves both R and the CLI and
survives a fresh R session / pod restart. Configured via VMX_OIDC_ISSUER,
VMX_OIDC_CLIENT_ID, VMX_OIDC_SCOPES.
config_yaml as inline YAML text, matching the current API form
contract.from-text
creation endpoints.First broadly functional release: the client now covers the full analysis
workflow — treatments, studies, datasets/prep, data-versions, modeling-data
tables, NCA, modeling (build runs + fits + estimates), simulation, and the
study analysis log — with typed S3 objects, tibble collections, async polling,
and classed errors, all validated against the real API (staging). Known
deferred pieces: the nlmixr2/Torsten data adapters, vmx_dataset_download, and
tibble reshaping of the VPC artifact (each needs real-data validation).
vmx_analysis_log() (GET /studies/{id}/analysis-log,
paginated → tibble; since accepts a POSIXct/Date or ISO-8601 string;
resource accepts an id or object).vmx_fit_obs_vs_pred() now reshapes the PK observation block
into a one-row-per-observation tibble (predicted-concentration bands on the
"extra" attribute, PD block on "pd"). vmx_fit_vpc() still returns the
parsed nested artifact.vmx_client() connection object with redacted-token printing, the
vmx_error condition hierarchy, and the design proposal under docs/.{error:{...}}
envelope mapping onto vmx_auth_error/vmx_api_error, and next_cursor
pagination. Implemented vmx_whoami(), vmx_health(), the vmx_treatments()
/ vmx_treatment() / vmx_treatment_create() / vmx_treatment_update()
verbs, vmx_upload() (streamed multipart), vmx_prep_status(), and
vmx_wait() (dataset/prep-status poller). Typed S3 resource objects with
print/as_tibble, id-or-object resolution with prefix validation, and
collection→tibble conversion. httr2-mocked unit tests plus an opt-in live
smoke test (VMX_RUN_LIVE_TESTS=1).vmx_studies() / vmx_study() /
vmx_study_create() / vmx_study_update(); vmx_data_versions() /
vmx_data_version() / vmx_data_version_create() (format job over an upload
composition, returns a prep-status) / vmx_data_version_archive() /
vmx_data_version_unarchive() / vmx_data_version_export() (signed-URL
envelope, optional streamed download). Adds a vmx_patch() HTTP helper and
vmx_opt_id() (NULL-passthrough id resolution). The vmx_data_version_table()
and modeling-data adapters (§6.5a) remain stubbed for a dedicated slice.vmx_nca_analyses() (data-version/study/treatment/status/time-basis
filters), vmx_nca() (create + optionally block via vmx_wait()),
vmx_nca_get(), and vmx_nca_result() (reshapes point_estimates into a tidy
one-row-per-subject tibble, quantity metadata on the "quantities" attribute).
vmx_wait() gains an NCA method (terminal completed/degraded succeed,
failed raises). The status poller is refactored into a generic
vmx_poll_status() shared by the dataset and NCA waiters.vmx_model_catalog() (categories flattened to a tibble),
vmx_model_describe(), vmx_modeling_options(); vmx_model_build() (parses
the "GEN_uuid:increasing" pd-marker shorthand, create + optional
vmx_wait()), vmx_model_build_runs() / _status() / _results() /
_logs() / _export() / _report() / _cancel(); and fits:
vmx_model_fits(), vmx_model_fit() (details), vmx_fit_subject_estimates()
and vmx_fit_global_estimates() (reshaped to tidy tibbles with point
estimate + credible interval). vmx_wait() gains a model-build-run method
(terminal succeeded/degraded succeed; failed/cancelled raise). The
vmx_nca() / vmx_model_build() wait = TRUE paths now forward polling
controls (timeout/interval/progress) to vmx_wait().
vmx_fit_obs_vs_pred() and vmx_fit_vpc() return the parsed diagnostic
artifacts as-is for now; reshaping their nested plot bands into tibbles is a
follow-up.vmx_dosing_input(); vmx_sim_existing_subject(),
vmx_sim_hypothetical_subject(), vmx_sim_population() (accept a data.frame
of subjects/covariates or explicit records; create + optional vmx_wait());
vmx_sim_status(), vmx_sim_result(), vmx_sim_cancel(). vmx_wait() gains
a simulation-job method (terminal succeeded; failed/cancelled raise).
vmx_sim_result() returns the parsed result payload (reshaping deferred).vmx_dataset_files(), vmx_dataset_tags() (key/value
tibble), vmx_dataset_cancel(), vmx_upload_ignore() / vmx_upload_unignore()
(now take dataset + upload), vmx_prep_questions() (prompt fields →
tibble), and vmx_prep_answer() (posts a named-list answers body).
vmx_dataset_download() stays unimplemented — the files listing exposes no
per-file download URL (use vmx_data_version_export()).vmx_data_version_table() (any of the subjects/pk/
dosing/pd/labs/covariates domains → a typed tibble, column metadata on
the "columns" attribute), the vmx_subjects() / vmx_pk() / vmx_pd()
accessors, and vmx_model_data() (bundles the available tables + DV metadata
as a vmx_model_data object, fetching only table_availability domains). The
nlmixr2 (vmx_nlmixr_data()) and Stan/Torsten (vmx_torsten_data()) adapters
remain deferred stubs — the NONMEM/ragged-array assembly must be validated
against real data + the DV column manifest before shipping.