Generated analysis files are saved in
pharmflux-r-tutorial-output alongside the Rmd file. The
plot, tables and fit statuses appear in the rendered HTML. Use
rmarkdown::render("R-USER-GUIDE.Rmd") to rerun the full
analysis.
Build a unit-aware model, simulate doses, compare parameters, and fit synthetic observations using PharmFlux’s R package. The native engine runs locally; these examples require no Ubi account, Julia runtime, or simulation service. All parameter values and data in this guide are synthetic.
This guide targets the source-based 0.1.0 R API. The R binding is a development package; these instructions do not assume a CRAN or R-universe release exists. Render this R Markdown file after installing PharmFlux, jsonlite, knitr, and rmarkdown. Installation chunks are displayed but are not executed; all analysis chunks execute in order. The complete example script runs the same workflow and saves its outputs. Keep this guide with the source version you use.
You need R, its compatible C compiler toolchain, Rust 1.98 or later,
Cargo, and a complete PharmFlux source checkout. The local installation
uses the Rust workspace outside bindings/r; copying that
directory by itself is insufficient. Build from the repository root.
Install the R serialization dependency in your usual R session:
install.packages("jsonlite")
In a macOS or Linux terminal, select a user library and install the package:
mkdir -p "$HOME/R/pharmflux-library"
export R_LIBS_USER="$HOME/R/pharmflux-library"
export R_HOME="$(R RHOME)"
cargo fetch --locked
cargo build -p pharmflux-r --release --locked
R CMD INSTALL --library="$R_LIBS_USER" bindings/r
Rscript -e 'library(pharmflux); print(packageVersion("pharmflux"))'
The expected version for this source is 0.1.0. The R
build uses --offline --locked, so the initial
cargo fetch --locked matters. An offline build still needs
its locked registry dependencies cached in CARGO_HOME. The
native library links to your installed R runtime; use the compiler
toolchain appropriate to that R distribution.
In a new RStudio session, include the installation library before loading:
.libPaths(c(path.expand("~/R/pharmflux-library"), .libPaths()))
library(pharmflux)
library(jsonlite)
packageVersion("pharmflux")
The local build has been checked on macOS ARM64. Do not interpret this as a Linux or Windows installation guarantee. On Windows, compatible R build tools and the Rust toolchain are required; cross-platform source installation needs its own verification. Restart R after replacing a loaded native package.
If you have the development packaging script, a standalone source archive can be prepared with its own bundled Rust workspace:
# The source destination must not already exist.
python3 scripts/prepare-r-source.py /tmp/pharmflux-r-source
mkdir -p /tmp/pharmflux-r-library
R CMD INSTALL --library=/tmp/pharmflux-r-library \
/tmp/pharmflux-r-source/pharmflux_0.1.0.tar.gz
This archive still requires cached Cargo registry dependencies; it does not bundle every registry dependency. See R binding installation notes for packaging details and the exact qualification scope.
The model stores central drug amount in mg and reports concentration
cp in mg/L. Clearance is 0.3 L/h and volume is 3 L, so the
elimination rate is 0.1 per hour. After a 6 mg bolus,
cp(t) = 2 exp(-0.1 t).
library(pharmflux)
library(jsonlite)
source <- "model quickstart_one_compartment
time_unit = h
parameters
cl = 0.3 [L/h] log
v = 3 [L] log
states
central = 0 [mg]
dynamics
d/dt(central) = input_central - cl / v * central
outputs
cp = central / v [mg/L]
dosing
central [mg] = 1
end
"
model <- compile_model(source)
q <- function(value, unit) list(value = value, unit = unit)
The dosing declaration enables delivery to central; the
input_central term makes an infusion contribute to the
dynamics. A bolus applies an amount jump. Model text, JSON model text,
and appropriately structured R lists are accepted. A file path is not
source text: read it with
paste(readLines("model.pfx", warn = FALSE), collapse = "\n").
Explicit observations choose exact times and sides. All six observations here are post-event readouts, including the concentration immediately after the time-zero bolus.
times <- c(0, 1, 2, 4, 8, 12)
request <- list(
schema = "pharmflux.run/v0.1",
solver = "diffsol_bdf",
budgets = list(solver_callbacks = 1000000L,
events = 100L, output_values = 10000L),
regimen = list(
end = q(12, "h"), samples = list(),
observations = lapply(times, function(t) list(time = q(t, "h"), side = "post")),
administrations = list(list(
target = "central", time = q(0, "h"), amount = q(6, "mg"),
delivery = list(kind = "bolus"), bioavailability = 1
)),
rtol = 1e-10, atol = 1e-12
)
)
result <- simulate_model(model, request)
frame <- result_data_frame(result)
cp <- frame[frame$OUTPUT == "cp", ]
stopifnot(
identical(cp$TIME, as.double(times)),
all(cp$SIDE == "post"),
max(abs(cp$VALUE - 2 * exp(-0.1 * cp$TIME))) < 1e-8
)
print(cp)
## TIME SIDE OUTPUT VALUE UNIT
## 1 0 post cp 2.0000000 mg/L
## 2 1 post cp 1.8096748 mg/L
## 3 2 post cp 1.6374615 mg/L
## 4 4 post cp 1.3406401 mg/L
## 5 8 post cp 0.8986579 mg/L
## 6 12 post cp 0.6023884 mg/L
Expected concentrations are 2.000000 mg/L at 0 h, 1.340640 mg/L at 4 h, and 0.602388 mg/L at 12 h. The analytic comparison checks scientific behavior as well as successful execution.
Compile once and reuse model. Each simulation starts
from its own initial conditions and request. It does not continue the
previous trajectory. model holds a native pointer belonging
to this R session; recreate it from source in another session or
process.
The binding encodes lists with
jsonlite::toJSON(auto_unbox = TRUE). Use unnamed lists for
arrays of records and named lists for objects. For example,
administrations = list(list(...)) must remain an array even
when it contains one dose. For a one-parameter sensitivity selection,
use list("cl"); a length-one character vector may be
encoded as a scalar. Keep tolerances as doubles and row/budget counts as
whole numbers.
Read versioned JSON without simplifying nested records:
# Optional file workflow after saving a request later in this guide:
# request <- fromJSON("run.request.json", simplifyVector = FALSE)
Unknown fields and malformed arrays are rejected. NA is
not an ordinary scientific quantity; handle missing measurements
explicitly through MDV in the dataset adapter.
result_data_frame() returns long data with
TIME, SIDE, OUTPUT,
VALUE, and UNIT. It attaches
time_unit and execution_identity attributes
and preserves missing numeric values as NA. Output names
cannot overwrite the table’s metadata columns because each name appears
in OUTPUT.
plot(cp$TIME, cp$VALUE, type = "b",
xlab = paste0("Time (", attr(frame, "time_unit"), ")"),
ylab = "cp (mg/L)")
write.csv(frame, "trajectory.csv", row.names = FALSE)
Filter the desired output before plotting. Retain side labels when comparing values at an event time. States are returned only when exposed as model outputs. CSV does not preserve execution attributes, so retain the complete JSON result as well.
For a tidyverse workflow, the existing data frame can be passed to dplyr or ggplot2. Those packages are optional; they are not required for the native engine or the examples in this guide.
R lists use copy-on-modify semantics, so assigning
changed <- request and editing a nested field creates a
changed experiment without altering the original request. The native
model pointer still refers to the same compiled model.
changed <- request
changed$parameters <- list(cl = q(0.6, "L/h"), v = q(3, "L"))
faster_clearance <- simulate_model(model, changed)
repeated <- request
repeated$regimen$administrations[[1]][["repeat"]] <- list(
interval = q(4, "h"), additional = 2L
)
repeated_result <- simulate_model(model, repeated) # Doses at 0, 4, and 8 h.
infusion <- request
infusion$regimen$administrations[[1]]$delivery <- list(
kind = "infusion",
span = list(kind = "duration", duration = q(2, "h"))
)
infusion_result <- simulate_model(model, infusion) # 6 mg over 2 h.
additional counts administrations after the first.
Rate-based infusion uses
span = list(kind = "rate", rate = q(3, "mg/h")) instead of
duration. Supply exactly one span type. Bioavailability is a fraction
from 0 to 1; the delivered amount also passes through the model’s
amount-to-state scale. Request lag and model-declared lag add
together.
Compatible units convert explicitly: an administration in ug can target a dosing map in mg. Amount and concentration have different dimensions; use a declared volume or dosing scale to connect them. A positive infusion rate needs amount/time units. Consult the unit specification before using a new unit spelling.
Declare covariates in the model and supply quantity values through
regimen$covariates; step covariates can also use
covariate_changes. Derived individual
expressions are recomputed from their inputs and cannot be directly
overridden. Constant and step numeric covariates are supported;
categorical covariates and linear interpolation are outside the current
scope.
To model first-order absorption with central/peripheral distribution, add a depot, intercompartmental clearance, and peripheral volume. Expose amounts as outputs so the amount balance can be inspected alongside concentration.
oral_source <- "model quickstart_oral_two_compartment
time_unit = h
parameters
cl = 1 [L/h] log
vc = 5 [L] log
vp = 10 [L] log
exchange = 0.5 [L/h] log
ka = 1 [1/h] log
states
depot = 0 [mg]
central = 0 [mg]
peripheral = 0 [mg]
dynamics
d/dt(depot) = -ka * depot
d/dt(central) = ka * depot - cl / vc * central - exchange * (central / vc - peripheral / vp)
d/dt(peripheral) = exchange * (central / vc - peripheral / vp)
outputs
cp = central / vc [mg/L]
depot_amount = depot [mg]
central_amount = central [mg]
peripheral_amount = peripheral [mg]
dosing
depot [mg] bolus = 1
end
"
oral_model <- compile_model(oral_source)
oral_request <- request
oral_request$regimen$administrations[[1]]$target <- "depot"
oral_request$regimen$administrations[[1]]$amount <- q(100, "mg")
oral_request$regimen$administrations[[1]]$bioavailability <- 0.8
oral_result <- simulate_model(oral_model, oral_request)
oral_frame <- result_data_frame(oral_result)
stopifnot(
oral_frame$VALUE[oral_frame$TIME == 0 & oral_frame$OUTPUT == "cp"] == 0,
oral_frame$VALUE[oral_frame$TIME == 0 & oral_frame$OUTPUT == "depot_amount"] == 80,
all(oral_frame$VALUE >= -1e-8)
)
The available 80 mg enters the depot and reaches central through first-order absorption. The depot permits bolus delivery only; there is no infusion forcing in its equation. Adding the three amount derivatives cancels internal transfers and leaves central elimination. These are simple synthetic mechanism assumptions, not drug-specific parameter estimates. For larger PBPK/QSP systems, inspect conservation and domains and use state-specific tolerances.
At a bolus time, pre selects the amount before the jump
and post selects it after the jump. Dataset row order never
substitutes for this choice.
at_dose <- request
at_dose$regimen$observations <- list(
list(time = q(0, "h"), side = "pre"),
list(time = q(0, "h"), side = "post")
)
event_result <- simulate_model(model, at_dose)
stopifnot(identical(unlist(event_result$outputs[[1]]$values), c(0, 2)))
An explicit observation plan preserves order and duplicates. For
trajectory sampling, omit observations and populate
samples; event boundaries may add rows. Always inspect
returned times and sides. Do not combine nonempty samples
with an observation plan, or supply an empty observation plan.
This guide uses BDF. Simulation also supports
diffsol_tsit45, diffsol_esdirk34,
diffsol_tr_bdf2, diffsol_rosenbrock23, and
diffsol_rodas5p within their supported ODE scope. No
unknown method selects a fallback. Sensitivity-based fitting currently
requires BDF. An analytic_linear_pk run needs a model that
declares linear_pk; an arbitrary linear-looking
differential equation does not select that engine automatically.
rtol is dimensionless, while scalar atol
uses each state’s numeric unit. For differing state scales, remove
atol and supply a complete quantity map:
absolute_tolerances = list(central = q(1e-12, "mg")) for
our single state. Use exactly one absolute-tolerance mode. Check
numerical stability by comparing tighter tolerances and an independent
or analytic reference where possible.
Budgets constrain solver callbacks, events, and output values. They are work limits rather than deadlines. More work cannot correct invalid dimensions, equation domains, or an uninformative fitting design.
Scans evaluate up to 25 explicit parameter points, each with a unique ID. The base run has per-run budgets, and the scan also needs aggregate budgets.
scan_request <- list(
schema = "pharmflux.scan/v0.1", run = request,
points = lapply(c(0.3, 0.6, 1.2), function(value) list(
id = paste0("cl_", value), parameters = list(cl = q(value, "L/h"))
)),
total_budgets = list(solver_callbacks = 3000000L,
events = 300L, output_values = 30000L)
)
scan <- scan_model(model, scan_request)
scan_table <- do.call(rbind, lapply(scan$results, function(point) {
data <- result_data_frame(point$result)
data$POINT <- point$id
data
}))
print(scan_table[scan_table$TIME == 12, ])
## TIME SIDE OUTPUT VALUE UNIT POINT
## 6 12 post cp 0.60238842 mg/L cl_0.3
## 12 12 post cp 0.18143591 mg/L cl_0.6
## 18 12 post cp 0.01645949 mg/L cl_1.2
A scan compares deterministic scenarios; it does not provide population variability or confidence intervals. Use biologically defensible ranges.
Morris screening calculates elementary effects over chosen parameter ranges at one output row. Two parameters and two trajectories require six runs here.
morris_request <- list(
schema = "pharmflux.morris/v0.1",
problem = list(kind = "generated", request = list(
run = request,
ranges = list(
list(parameter = "cl", lower = q(0.1, "L/h"), upper = q(0.7, "L/h")),
list(parameter = "v", lower = q(3, "L"), upper = q(6, "L"))
),
trajectory_count = 2L, num_levels = 4L, seed = 42L,
total_budgets = list(solver_callbacks = 6000000L,
events = 600L, output_values = 60000L),
output = "cp", row = 3L # Zero-based: the selected 4 h observation.
))
)
morris <- morris_model(model, morris_request)
print(morris$result$effects)
## [[1]]
## [[1]]$effects
## [[1]]$effects[[1]]
## [1] -0.4550333
##
## [[1]]$effects[[2]]
## [1] -0.3284635
##
##
## [[1]]$mu
## [1] -0.3917484
##
## [[1]]$mu_star
## [1] 0.3917484
##
## [[1]]$parameter
## [1] "cl"
##
## [[1]]$sigma
## [1] 0.08949838
##
## [[1]]$unit
## [1] "mg/L"
##
##
## [[2]]
## [[2]]$effects
## [[2]]$effects[[1]]
## [1] -0.9639105
##
## [[2]]$effects[[2]]
## [1] -0.289897
##
##
## [[2]]$mu
## [1] -0.6269038
##
## [[2]]$mu_star
## [1] 0.6269038
##
## [[2]]$parameter
## [1] "v"
##
## [[2]]$sigma
## [1] 0.4765995
##
## [[2]]$unit
## [1] "mg/L"
mu_star is average absolute effect magnitude;
sigma describes variation across elementary effects. Their
interpretation depends on the output, sampling row, ranges, design grid
and seed. The 25-run scan bound also limits Morris designs. The R API
does not expose a standalone forward-sensitivity runner;
fit_model() compiles the sensitivity selection needed for a
fit.
This example estimates clearance and volume from exact synthetic
observations generated with cl = 0.6 L/h and
v = 4.5 L. It uses a fixed additive normal SD of 0.1 mg/L.
A real analysis needs an error model justified for its measurement
process.
sensitivity_request <- list(
schema = "pharmflux.sensitivity/v0.1", run = request,
with_respect_to = list("cl", "v"),
derivative_absolute_tolerances = list(
cl = list(central = q(1e-12, "mg.h/L")),
v = list(central = q(1e-12, "mg/L"))
)
)
observations <- lapply(seq_along(times), function(index) list(
row = index - 1L, output = "cp",
value = q(6 / 4.5 * exp(-0.6 * times[index] / 4.5), "mg/L"),
error = list(additive_sd = q(0.1, "mg/L"), proportional_sd = 0)
))
fit_request <- list(
schema = "pharmflux.fit/v0.1",
problem = list(kind = "individual", request = list(
objective = list(simulation = sensitivity_request,
observations = observations, priors = list()),
bounds = list(
list(parameter = "cl", lower = q(0.05, "L/h"), upper = q(1.2, "L/h")),
list(parameter = "v", lower = q(1, "L"), upper = q(8, "L"))
),
max_evaluations = 400L, max_iterations = 100L, gradient_tolerance = 1e-5
))
)
fitted <- fit_model(model, fit_request)
print(fitted$fit$status)
## [1] "converged"
print(fitted$fit$parameters)
## $cl
## $cl$unit
## [1] "L/h"
##
## $cl$value
## [1] 0.6
##
##
## $v
## $v$unit
## [1] "L"
##
## $v$value
## [1] 4.5
stopifnot(
fitted$fit$status == "converged",
abs(fitted$fit$parameters$cl$value - 0.6) < 1e-4,
abs(fitted$fit$parameters$v$value - 4.5) < 1e-4
)
replay <- request
replay$parameters <- fitted$fit$parameters
fit_predictions <- simulate_model(model, replay)
Native row values start at zero even in R. Our explicit
observation plan makes row = 0 the post-dose measurement at
0 h. Derivative absolute tolerances have state-unit divided by
parameter-unit, rather than the state’s unit alone. The selected
parameter order must match the fitting contract.
The engine can return a result with evaluation_limit;
inspect the status before using estimates. Review objective, work count,
boundaries and prediction replay. A fit to exact noiseless synthetic
data verifies implementation; it does not establish confidence-interval
calibration or clinical validity. priors = list() requests
likelihood fitting. Supported priors produce MAP fits; consult Gaussian objectives for their native
representation. The R binding does not currently expose Python’s
derivative-free scalar fitting method.
The R helpers convert a restricted record format into explicit requests. They are useful for tabular PK workflows, but do not import a NONMEM control stream or infer endpoint, covariate, censoring or missing-value semantics.
To use an existing CSV instead, read and validate it before calling
the adapters. Preserve string IDs such as 001, rather than
converting them to numbers:
# Replace the synthetic data below with your own validated file when needed.
# data <- read.csv("observations.csv", stringsAsFactors = FALSE,
# colClasses = c(ID = "character"))
# Check numeric TIME/AMT/DV, explicit MDV, CMT mappings and declared units.
Construct dose and observation rows with separate CMT values. This example has two subjects, both following the same synthetic concentration profile.
data <- do.call(rbind, lapply(c("A", "B"), function(id) {
data.frame(
ID = id, TIME = c(0, 0, 1, 2, 4), EVID = c(1L, 0L, 0L, 0L, 0L),
CMT = c(1L, 2L, 2L, 2L, 2L), AMT = c(6, 0, 0, 0, 0),
MDV = c(1L, 0L, 0L, 0L, 0L),
DV = c(NA_real_, 6 / 4.5 * exp(-0.6 * c(0, 1, 2, 4) / 4.5))
)
}))
rownames(data) <- NULL
run_template <- request
run_template$regimen$samples <- list()
run_template$regimen$observations <- NULL
run_template$regimen$administrations <- list()
dataset <- nonmem_requests(
data, run_template, compartments = c("1" = "central"),
time_unit = "h", amount_unit = "mg", observation_side = "post"
)
predicted_data <- simulate_data(model, dataset)
print(head(predicted_data$data))
## TIME SIDE OUTPUT VALUE UNIT ID SOURCE_ROW
## 1 0 post cp 2.000000 mg/L A 2
## 2 1 post cp 1.809675 mg/L A 3
## 3 2 post cp 1.637462 mg/L A 4
## 4 4 post cp 1.340640 mg/L A 5
## 5 0 post cp 2.000000 mg/L B 7
## 6 1 post cp 1.809675 mg/L B 8
The dataset supplies doses and selected observations. The template supplies parameters, static covariates, horizon, solver, and budgets. Its schedules, including resets, covariate changes and checkpoints, must be empty. Budgets apply per subject and subjects execute sequentially with one compiled model.
Required columns are ID, TIME, and EVID. Optional supported columns
are DV, MDV, AMT, CMT, RATE, ADDL, II, and SS. EVID = 0,
MDV = 0 selects a measurement; MDV 1 excludes it.
EVID = 1 is a dose with positive AMT and a mapped CMT. RATE
0 is bolus; positive RATE requires explicit rate_unit and
describes a finite infusion. ADDL/II expand repeats. Nonzero SS, EVID
2/3/4, negative RATE, unknown columns, and dose fields on observation
rows fail. The converter does not extend the template’s time
horizon.
Clear the individual fit template’s run and observations so that the
prepared dataset can supply them. Map observation CMT 2 to output
cp and its error model. This endpoint mapping is separate
from dose CMT 1 to central.
fit_template <- fit_request
fit_template$problem$request$objective$simulation$run <- NULL
fit_template$problem$request$objective$observations <- list()
endpoints <- list("2" = list(
output = "cp", value_unit = "mg/L",
error = list(additive_sd = q(0.1, "mg/L"), proportional_sd = 0)
))
prepared <- fit_data_requests(dataset, fit_template, endpoints, mode = "pooled")
data_fit <- fit_data(model, prepared)
stopifnot(data_fit$results[[1]]$fit$status == "converged")
print(fit_parameter_data_frame(data_fit))
## ID PARAMETER ESTIMATE UNIT STATUS OBJECTIVE
## 1 <NA> cl 0.6 L/h converged -11.06917
## 2 <NA> v 4.5 L converged -11.06917
diagnostics <- fit_observation_data_frame(model, data_fit)
print(head(diagnostics))
## ID SOURCE_ROW RESULT_ROW TIME SIDE OUTPUT DV DV_UNIT PRED PRED_UNIT
## 1 A 2 0 0 post cp 1.333333 mg/L 1.333333 mg/L
## 2 A 3 1 1 post cp 1.166898 mg/L 1.166898 mg/L
## 3 A 4 2 2 post cp 1.021238 mg/L 1.021238 mg/L
## 4 A 5 3 4 post cp 0.782195 mg/L 0.782195 mg/L
## 5 B 7 0 0 post cp 1.333333 mg/L 1.333333 mg/L
## 6 B 8 1 1 post cp 1.166898 mg/L 1.166898 mg/L
## RESID
## 1 2.364267e-10
## 2 1.358151e-10
## 3 -8.893486e-11
## 4 -4.483406e-10
## 5 2.364267e-10
## 6 1.358151e-10
Use mode = "individual" for a separate fit per subject
and inspect every status. Pooled fitting shares selected parameters
across subjects and has no random effects. A pooled MAP prior is counted
once; separate individual MAP fits each receive the prior.
SOURCE_ROW is one-based in the original R data, while
RESULT_ROW is zero-based in the native result. Duplicate
observations remain separate likelihood terms. Included DV values must
be finite; set MDV explicitly to exclude missing measurements.
fit_observation_data_frame() replays converged fits and
incurs an additional simulation per subject. It preserves observation
sides and reports separate DV/PRED units. RESID is
NA when those units differ; the table does not silently
convert them for subtraction.
FOCEI and SAEM use one population request with per-subject regimens
and a lognormal random-effect contract. This four-subject example
estimates typical clearance, holding volume and eta variance fixed.
Fixing omega = 0.04 means eta SD is 0.2 on the log scale;
it is not a clearance-unit variance.
etas <- c(-0.22, -0.08, 0.08, 0.22)
population_rows <- do.call(rbind, lapply(seq_along(etas), function(index) {
t <- c(1, 2, 4)
data.frame(
ID = paste0("S", index), TIME = c(0, t), EVID = c(1L, 0L, 0L, 0L),
CMT = c(1L, 2L, 2L, 2L), AMT = c(6, 0, 0, 0), MDV = c(1L, 0L, 0L, 0L),
DV = c(NA_real_, 6 / 4.5 * exp(-0.6 * exp(etas[index]) * t / 4.5))
)
}))
rownames(population_rows) <- NULL
pop_sensitivity <- sensitivity_request
pop_sensitivity$with_respect_to <- list("cl")
pop_sensitivity$derivative_absolute_tolerances$v <- NULL
pop_sensitivity$run <- run_template
pop_sensitivity$run$parameters <- list(cl = q(0.5, "L/h"), v = q(4.5, "L"))
population_template <- list(
schema = "pharmflux.fit/v0.1",
problem = list(kind = "focei", request = list(
subjects = list(list(simulation = pop_sensitivity,
observations = list(), priors = list())),
fixed_effects = list(list(parameter = "cl", lower = q(0.35, "L/h"),
upper = q(0.85, "L/h"))),
random_effects = list(list(parameter = "cl")),
omega = list(list(0.04)), omega_diagonal_bounds = list(list(0.04, 0.04)),
eta_bound = 1.5, max_evaluations = 30000L,
max_iterations = 80L, gradient_tolerance = 0.005, seed = 17L
))
)
population_endpoints <- list("2" = list(
output = "cp", value_unit = "mg/L",
error = list(additive_sd = q(0.05, "mg/L"), proportional_sd = 0)
))
population_prepared <- population_fit_data_requests(
population_rows, population_template, compartments = c("1" = "central"),
endpoints = population_endpoints, time_unit = "h", amount_unit = "mg",
observation_side = "post"
)
population_fit <- fit_population_data(model, population_prepared)
population <- population_fit$result$population
print(population$status)
## [1] "converged"
print(population$objective_kind)
## [1] "focei_objective_function_value"
print(population$fixed_effects)
## $cl
## $cl$unit
## [1] "L/h"
##
## $cl$value
## [1] 0.6
stopifnot(
population$status == "converged",
abs(population$fixed_effects$cl$value - 0.6) < 0.06
)
subject_table <- population_fit_subject_data_frame(population_fit)
population_diagnostics <- population_fit_observation_data_frame(population_fit)
print(subject_table)
## SUBJECT_INDEX ID PARAMETER ETA FIXED_EFFECT INDIVIDUAL_ESTIMATE UNIT
## 1 0 S1 cl -0.17097502 0.6 0.5057056 L/h
## 2 1 S2 cl -0.06407457 0.6 0.5627610 L/h
## 3 2 S3 cl 0.06599456 0.6 0.6409325 L/h
## 4 3 S4 cl 0.18544253 0.6 0.7222506 L/h
## NEG_LOG_LIKELIHOOD STATUS
## 1 -6.127235 converged
## 2 -6.217687 converged
## 3 -6.218783 converged
## 4 -6.149588 converged
print(head(population_diagnostics))
## SUBJECT_INDEX ID SOURCE_ROW OBSERVATION_INDEX RESULT_ROW TIME TIME_UNIT SIDE
## 1 0 S1 2 0 0 1 h post
## 2 0 S1 3 1 1 2 h post
## 3 0 S1 4 2 2 4 h post
## 4 1 S2 6 0 0 1 h post
## 5 1 S2 7 1 1 2 h post
## 6 1 S2 8 2 2 4 h post
## OUTPUT DV DV_UNIT PRED PRED_UNIT IPRED IPRED_UNIT RESID
## 1 cp 1.1980312 mg/L 1.166898 mg/L 1.1916073 mg/L 0.03113347
## 2 cp 1.0764591 mg/L 1.021238 mg/L 1.0649459 mg/L 0.05522133
## 3 cp 0.8690732 mg/L 0.782195 mg/L 0.8505824 mg/L 0.08687821
## 4 cp 1.1789213 mg/L 1.166898 mg/L 1.1765943 mg/L 0.01202357
## 5 cp 1.0423916 mg/L 1.021238 mg/L 1.0382806 mg/L 0.02115384
## 6 cp 0.8149352 mg/L 0.782195 mg/L 0.8085199 mg/L 0.03274027
## IRES
## 1 0.006423943
## 2 0.011513177
## 3 0.018490781
## 4 0.002327046
## 5 0.004111045
## 6 0.006415302
Inspect population_prepared$request before fitting. Its
mapping preserves one-based SOURCE_ROW and zero-based subject,
observation and result indices. Subject order follows first appearance
in the input. FOCEI diagnostics report DV, population prediction PRED,
individual prediction IPRED, and source rows. Subject tables include eta
and the corresponding individual parameter. Residuals are missing when
measurement and prediction units differ.
Allowing Omega bounds to vary changes this example into variance estimation and requires a sufficiently informative cohort. Two random effects, error estimation and uncertainty have method-specific limits; do not infer support for an arbitrary nlmixr2 model or fitting configuration.
The same adapter can construct SAEM requests. Eight iterations intentionally illustrate a limited result; they are not a recommended estimation budget.
saem_template <- population_template
saem_template$problem$kind <- "saem"
saem_template$problem$request$max_iterations <- 8L
saem_prepared <- population_fit_data_requests(
population_rows, saem_template, compartments = c("1" = "central"),
endpoints = population_endpoints, time_unit = "h", amount_unit = "mg",
observation_side = "post"
)
saem_fit <- fit_population_data(model, saem_prepared)
saem <- saem_fit$result$population
print(saem$status)
## [1] "iteration_limit"
print(saem$objective_kind)
## [1] "saem_marginal_objective_function_value"
saem_subjects <- population_fit_subject_data_frame(saem_fit)
saem_observations <- population_fit_observation_data_frame(saem_fit)
A converged result reports
saem_marginal_objective_function_value. A limited run may
return saem_complete_data_surrogate; do not compare it as
if it were a final marginal objective. saem_observations
can be NULL when the native result supplies no
fitted-observation rows. Inspect the returned object and method-specific
status before requesting diagnostics or interpreting fits.
The native SAEM contract includes one- and two-effect lognormal subject effects with Gaussian residual error and supported common additive-error estimation. This public version does not support lognormal observation errors, censored observations, a fitted-covariate request field, or SAEM uncertainty. See the SAEM guide for exact combinations and limitations. Record seeds, examine stability across seeds, and verify recovery on a cohort representative of your intended use.
Preserve source, requests, complete results, rows, mappings, and software versions. Do not serialize a native pointer as the portable model artifact.
dir.create("pharmflux-analysis", showWarnings = FALSE)
writeLines(source, "pharmflux-analysis/model.pfx")
save_json <- function(object, filename) {
write_json(object, file.path("pharmflux-analysis", filename),
auto_unbox = TRUE, pretty = TRUE, digits = NA, null = "null")
}
save_json(request, "run.request.json")
save_json(result, "run.result.json")
save_json(fit_request, "fit.request.json")
save_json(fitted, "fit.result.json")
save_json(population_prepared$request, "population.request.json")
save_json(population_fit$result, "population.result.json")
write.csv(population_fit$mapping, "pharmflux-analysis/population.mapping.csv", row.names = FALSE)
saveRDS(population_fit, "pharmflux-analysis/population-fit.rds")
write.csv(population_rows, "pharmflux-analysis/population.rows.csv", row.names = FALSE)
saveRDS(frame, "pharmflux-analysis/trajectory.rds")
writeLines(capture.output(sessionInfo()), "pharmflux-analysis/session-info.txt")
print(result$identity)
## $algorithm
## [1] "bdf"
##
## $backend
## [1] "diffsol"
##
## $backend_version
## [1] "0.17.1+sha256:c182fd25086e064bf9186282ae9db23981e39b3720cba12372ca9cd5e833fb9f"
##
## $bound_value_hash
## [1] "sha256:3acc58471af74194008bde05ff3dfccd5d9baeec34d29d91468ea8865e13bb17"
##
## $build_profile
## [1] "release"
##
## $compiler_source_sha256
## [1] "sha256:3d60dca0248b6cf661225b81a9c45e6419204da5bc25cc3e4c5c63d2918c1818"
##
## $compiler_version
## [1] "pharmflux/0.1.0"
##
## $model_content_hash
## [1] "sha256:75afccc84e2f2c1448a5432dec62406b7067dbb68b51b2882d98aa89e3cf1a6d"
##
## $run_request_hash
## [1] "sha256:4a248f684ef29c4fbbaac25049116180b45a92337113a8e099fc6879f4c3cf65"
##
## $rustc_version
## [1] "rustc 1.98.1 (48a229cea 2026-09-01)"
##
## $rustflags_sha256
## [1] "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
##
## $spec_version
## [1] "pharmflux.spec/0.1"
##
## $target
## [1] "x86_64-unknown-linux-gnu"
Save the native population_fit$result envelope as JSON;
the classed R wrapper and its source mapping are stored separately as
RDS and CSV. RDS retains data-frame attributes in an R workflow; JSON
retains the native execution identity for interchange. That identity
records model content, resolved bindings, the run request, compiler
source and backend/build details. It identifies the execution, not the
model’s scientific suitability.
To reload the simulation, read its request with
simplifyVector = FALSE, compile the saved source, and call
simulate_model() again. Do not share the compiled pointer
across processes or rely on it surviving saveRDS(). Compile
inside each parallel R worker. The tabular simulation helper itself
executes subjects sequentially.
Scientific compiler/engine failures use class
pharmflux_error with a code, message, and available
scientific/source-location fields. R adapter validation can instead
raise an ordinary R error. Catch the classes appropriate to the
operation you call.
invalid <- request
invalid$parameters <- list(cl = q(1, "h"))
caught <- tryCatch(
simulate_model(model, invalid),
pharmflux_error = function(error) {
print(error$code)
message(conditionMessage(error))
error
}
)
## [1] "unit"
## incompatible dimensions; mass/amount conversion requires explicit molar mass
stopifnot(inherits(caught, "pharmflux_error"))
stopifnot(identical(simulate_model(model, request), result))
Failures produce no partial scientific result. A failed request does not replace the compiled model or retain its bindings. Some errors have fallback line/column information rather than exact expression spans.
| Symptom | Action |
|---|---|
| R cannot find the package | Check .libPaths() and the library passed to
R CMD INSTALL. |
| Install fails to find Cargo or R headers | Check PATH, R_HOME, the complete workspace, and the
compiler toolchain for this R distribution. |
| Offline compilation fails | Fetch the locked Cargo dependencies before installation. |
| Missing helper after reinstall | Restart R and check find.package("pharmflux") for an
older library copy. |
| A one-element field becomes a JSON scalar | Use an unnamed list for the native array, particularly
with_respect_to. |
| Unit or unknown-symbol error | Compare model declarations, quantity dimensions and permitted input expressions. |
| Wrong dose-time value | Inspect SIDE and request pre/post explicitly. |
| Fit uses the wrong row | Convert from one-based R source rows to zero-based native result indices; inspect the adapter mapping. |
| Data preparation rejects DV | Use explicit MDV for excluded measurements; included values must be finite. |
| Dataset rejects columns or doses | Supply explicit CMT maps and RATE units; remove unsupported columns only after deciding how their information belongs in the model. |
| Fit hits limits or parameter bounds | Check identifiability, starting values, scientific bounds and error model before extending budgets. |
| Residuals are NA | Compare DV/PRED units and missingness; no implicit table-level unit conversion is performed. |
| SAEM observation table is NULL | Inspect status and native fitted-observation availability; a limited fit need not provide a table. |
| Task | R API | Result |
|---|---|---|
| Compile | compile_model(source) |
Session-local native model pointer |
| Simulate | simulate_model(model, request) |
Native result as an R list |
| Tabulate a simulation | result_data_frame(result) |
Long data and identity attributes |
| Individual, pooled or population fit | fit_model(model, request) |
Inspect fit or population status |
| Parameter scan | scan_model(model, request) |
Per-point simulation results |
| Morris screening | morris_model(model, request) |
Design and elementary effects |
| Prepare dose and measurement rows | nonmem_requests(...) |
Per-subject requests and source data |
| Simulate prepared rows | simulate_data(model, dataset) |
Subject results and combined data |
| Prepare individual or pooled fit | fit_data_requests(...) |
Fit requests and source mapping |
| Execute prepared fits | fit_data(model, prepared) |
Fit results and mappings |
| Tabulate estimates | fit_parameter_data_frame(fitted) |
Estimates, units, statuses and objective |
| Replay fitted observations | fit_observation_data_frame(model, fitted) |
Converged-fit observation diagnostics |
| Prepare population rows | population_fit_data_requests(...) |
One population request and mapping |
| Fit population rows | fit_population_data(model, prepared) |
Population result and mapping |
| Tabulate population subjects | population_fit_subject_data_frame(fitted) |
Eta and individual parameter values |
| Tabulate population observations | population_fit_observation_data_frame(fitted) |
Fitted-observation diagnostics or NULL |
Installed help is available through ?compile_model,
?nonmem_requests, ?fit_data_requests, and
?population_fit_data_requests. Read the model language, regimen contract, run and result identity, parameter scans, Morris screening, Gaussian objectives, and JSON schemas when adapting examples.
For Python colleagues, use the Python
user guide.