New features, fully backward compatible with CRAN 1.0.2 (verified
directly, inst/validation/13_backcompat_v1_0_2.R).
sim_tte_ode(): joint PK/PD and time-to-event simulation with
in-solver event detection. The survival probability is added to a
PK/PD mrgsolve model as one more ODE compartment (`dxdt_p11 = -p11
); each subject's event time is located *during* ODE integration, at the first internal solver evaluation where p11falls to or below a per-subject uniform drawU, rather than resolved afterward from a pre-simulated trajectory (the mechanism sim_tte_df()already used). Built-in models:"exponential", "weibull", "gompertz", and "mspline"` (a quadratic M-spline
baseline hazard evaluated continuously inside the compiled model, in
one of three shipped interior-knot-count variants: 3, 5, or 7)."pk_hazard"
(concentration-driven), "irm1_hazard"-"irm4_hazard"
(response-driven, the four indirect-response model types),
"tmdd_hazard" (driven by drug-target complex, target-mediated drug
disposition). Each reuses an unedited mrgsolve::modlib() PK/PD
backbone plus a hazard link; dosing is ordinary mrgsolve event data
via sim_tte_ode()'s data argument.sim_tte_ode()
(covariates/beta, optionally formula): an arbitrary named set
of log-linear covariates, lp(t) = sum_k beta_k * X_k(t),
generalizing sim_tte()'s single-covariate lp_data mechanism.
Supplying formula (e.g. ~ age + arm) builds the design matrix X
with stats::model.matrix() instead of using covariates' raw
columns directly, so factor coding, interactions, and any term linear
in beta (I(age^2), log(dose), splines::ns(weight, 3)) are
supported; beta's names must then match the design matrix's
columns. covariates may be baseline (one row per subject, no time
column) or time-varying, with either form of beta. An implied
intercept is dropped automatically (with a message), matching
survival::coxph()'s own convention, since the model's own baseline
hazard parameter already plays that role. formula = NULL (the
default) preserves the original interface exactly.omega) for the PK/PD hazard
library: a log-normal draw on any subset of a model's own
structural PK/PD parameters, applied via per-subject idata columns
-- no model file needs a declared $OMEGA block. A user-supplied
model that already declares its own $OMEGA (the classic
TVCL/ETA() idiom) goes through mrgsolve::omat() instead,
automatically.tte_model(): convert your own mrgsolve PK/PD model for use
with sim_tte_ode(), instead of being limited to the six built-in
models. Accepts model code, a file path, or a compiled model; adds
the survival compartment and in-solver event-detection scaffolding
without editing the model's own $ODE/$DES content (only
$GLOBAL/$MAIN, which mrgsolve allows only one of each, are
edited in place). Verified to reproduce all six built-in hazard
models exactly when converting their own mrgsolve::modlib()
backbones.add_censoring(): independent right censoring, on top of any
already-simulated events data frame (sim_tte_ode(), sim_tte(), or
sim_tte_df() output alike). Draws a per-subject censoring time from
an exponential, Weibull, uniform, lognormal, gamma, or user-supplied
distribution and takes min(event time, censoring time, administrative end); sim_status/sim_time are updated in place and a new
sim_reason column ("event"/"censored"/"administrative")
records why. censoring_rate_for() solves for the distribution
parameter (rate, Weibull scale at a fixed shape, lognormal meanlog at
a fixed sdlog, or gamma rate at a fixed shape) giving a target
censoring fraction, from the event times of an already-run uncensored
simulation. sim_tte_ode() also accepts a censoring = argument
that applies this automatically, inside its own seeded draw, so one
seed reproduces U, any between-subject draw, and censoring
together. $events now always carries sim_reason, even with no
censoring argument supplied ("event"/"administrative") --
sim_status/sim_time are unaffected either way. Dependent/informative
censoring (a censoring hazard driven by a subject's own simulated
PK/PD state) is out of scope for this release.add_interval_censoring(): interval censoring from a visit/
assessment-time schedule, on top of any already-simulated (and, if
used, already right-censored) events data frame. An event is only
known to have happened between the last event-free visit and the
first visit at which it was detected, giving (L, R]; a censored
subject is only known event-free through their last visit, giving
(L, Inf). Adds sim_time_left/sim_time_right columns --
sim_time/sim_status/sim_reason are unchanged, so every existing
consumer keeps working. visit_schedule() generates a per-subject
schedule with fixed spacing and optional random jitter, uniform
(default) or normal (jitter_dist = ; jitter is the half-width for
uniform, the standard deviation for normal) -- a jitter large
enough relative to the spacing to cross visit order raises a
warning() rather than silently reordering.
sim_tte_ode() also accepts a visits = argument (a schedule, or a
jitter spec) that applies this automatically, always after any
censoring = , inside its own seeded draw. Designed to feed
survival::Surv(time = sim_time_left, time2 = sim_time_right, type = "interval2") directly (convert the Inf upper bound to NA first --
see ?add_interval_censoring). An event with sim_time_left == 0
is a left-censored observation, with no separate mechanism needed
(see ?add_interval_censoring and the vignette's "Left censoring"
subsection). A visit-process model reacting to a subject's own
simulated PK/PD state, rather than the exact event time, is still out
of scope (see "C3" below).visit_schedule()'s jitter_dist = "normal" is now a truncated
normal, truncated at +/- jitter_trunc * jitter (new argument,
default 2), drawn via inverse-CDF. Both jitter_dist options are
now validated against every before any draw is made (uniform:
jitter < every / 2; normal: jitter_trunc * jitter < every / 2),
which guarantees jittered visits can never cross order or go
negative -- a jitter/jitter_trunc combination violating this
bound is now an error, not a warning() (the previous session's
reordering warning() is gone; it is now an unreachable internal
invariant for either built-in distribution). "uniform" output/seeds
are unchanged from the previous session; "normal" output/seeds
change (the draw mechanism itself changed to truncated inverse-CDF).thin_visits(visits, ...): missed visits and assessment dropout
("C1") on top of a visit schedule (the visits argument -- the
same schedule shape and name add_interval_censoring()/
sim_tte_ode() already use) -- reads no simulated outcome, so it can
never make an assessment schedule informative. Each non-baseline
visit is independently missed with probability p_miss; each subject
may independently start dropping out from a randomly chosen
non-baseline visit onward with probability p_dropout. Composes with
add_censoring() in either order; must run before
add_interval_censoring().visit_schedule_informative(visits, events, ...): an outcome-
reactive visit schedule ("C2") -- unlike thin_visits(), this reads
$events and lets visit attendance/timing react to the simulated
event time
(miss_near_event: a visit shortly before an event is missed at an
elevated rate; extra_visit_after_event: an unscheduled visit is
added shortly after an event, using the same distribution spec shape
as add_censoring()'s censoring). miss_near_event$p and
p_miss_base are additive/competing risks, not one overriding the
other: a near-window visit for an event subject is missed with
probability 1 - (1 - p_miss_base) * (1 - p) (collapses exactly to
p when p_miss_base = 0) -- pre-release seeds for a call combining
both changed accordingly. This makes the returned schedule
informative by construction; a message() naming this is emitted on
every call. See the vignette's "Informative assessment schedules"
subsection for a worked bias demonstration against thin_visits().
A third generator reacting to a subject's own PK/PD trajectory ("C3")
was evaluated and deferred to a later version.tte_model() before
sim_tte_ode(), then add_censoring() / add_interval_censoring()),
?simtte gains a short "Which functions go together" section, and the
model, events and name arguments say where their values come from.
No behaviour change.event_time_method argument on sim_tte_df() and sim_tte(),
with two values:
"grid" (the default, unchanged): the event time is the first
reported trajectory time at which survival falls to or below the
sampled uniform draw — exactly the existing behavior, byte-for-byte."log_survival" (opt-in): when a crossing occurs strictly after the
first reported observation, the event time is refined by linear
interpolation of cumulative hazard (H = -log(S)) between the two
reported points surrounding the crossing, i.e. assuming the hazard
is constant over that interval. For the M-spline model, whose hazard
is genuinely piecewise-constant on the reported grid, this recovers
the event time implied by that discretized hazard exactly. For
trajectories from a continuously-varying hazard (e.g. Weibull with
shape != 1, or a custom mechanistic model), it is an approximation
that improves as the reported time grid is refined.stats::runif(1) draw is consumed per
subject either way, so classification (event vs. censored) never
differs between methods for the same seed and trajectory.sim_tte() forwards event_time_method explicitly to its internal
sim_tte_df() call; it is unrelated to and never passed to the
mrgsolve simulation step.lp(t) for sim_tte() (lp_data argument): a
per-subject or population-level log hazard ratio trajectory,
last-observation-carried-forward between supplied time points.sim_tte_df(): simtte_example_model()/simtte_example_models()
load/list two bundled, user-editable mrgsolve .cpp files
demonstrating the pattern by hand (superseded, for new work, by
sim_tte_ode()'s in-solver mechanism and tte_model(), but kept as
a documented, tested way to use sim_tte_df() directly).omega/sigma errors. Calling sim_tte_ode(..., omega = <matrix>) (or sigma =) on a model with no matching
declared block previously surfaced mrgsolve's own cryptic
"improper signature: omat"; it now explains that omat()/smat()
only update an already-declared block and names what a fix looks
like.shape < 1. CRAN 1.0.2's
inst/models/weibull.cpp approximated the hazard as shape-independent
(constant-hazard) for solver time below 0.1, which was substantially
wrong for shape != 1 -- measured up to 0.16 absolute error in
survival probability for shape < 1 near t -> 0 in the tested
range. The survival probability is now computed as the exact
closed-form expression S(t) = exp(-exp(mu + lp) * t^shape)
directly, matching the analytical formula to floating-point precision
at every reported time, for every shape > 0.time argument now actually controls the output grid.
sim_tte()'s time argument previously only set end_time when
not supplied explicitly; the real output/event-time grid silently
came from mrgsolve's own default schedule (delta = 1) regardless
of the spacing requested in time, for both Weibull and M-spline
models. time now genuinely determines the simulation output grid
via mrgsolve's tgrid mechanism. This is a real, and often
substantial, change in reported sim_time values whenever a caller
supplied a time grid finer than delta = 1 (the common case, e.g.
the package's own README examples) -- measured directly in this
release's backward-compatibility audit
(inst/validation/13_backcompat_v1_0_2.R): 16.6% mean relative
difference in sim_time for the README Weibull example (shape = 1.1, itself unaffected by the shape < 1 fix above), 24.4% for a
shape = 2 case, 3.6% for the README M-spline example. sim_tte()'s
time default also changed from the scalar 100 to seq(0, 100, by = 1) (the same effective grid resolution as before when time is
not supplied, consistent with this fix)..... tgrid, obsonly, nocb,
carry_out/carry.out, and data can no longer be overridden via
... in sim_tte()/explore_pi_tq_surv()/.sim_surv_df()
(or, new in this release, sim_tte_ode()): each was confirmed to
silently defeat the package's output-grid or trajectory contract if
supplied by a caller. Doing so now raises a clear error naming the
argument..sim_surv_df() now calls mrgsim(..., nocb = FALSE)
(last-observation-carried-forward): the hazard value at time[i]
applies over [time[i], time[i+1]). Previously mrgsolve's default
(nocb = TRUE, next-observation-carried-backward) applied a hazard
value to the preceding interval, producing an incorrect survival
trajectory. See the "M-spline hazard carry convention" section of
?sim_tte.1e-8 of [0, 1] are clamped to exactly 0/1 before
monotonicity checking and event-time selection (values further outside
[0, 1] remain a hard error). This makes every accepted, normalized
trajectory safe for a -log(S) transform and is provably incapable
of changing event/censoring classification, given stats::runif()'s
[0, 1) support.basis/coefs/basehaz are
now validated (numeric, finite, matching dimensions, non-negative
resulting hazard) before reaching mrgsolve. Duplicate time values
are accepted only when they carry identical hazard values; conflicting
duplicates are a hard error.end_time boundary semantics resolved. end_time = 0 is now
accepted for both model types (a degenerate zero-duration follow-up;
every subject is censored at 0). For M-spline models, end_time must
now satisfy min(time) <= end_time <= max(time)
(end_time < min(time) was previously accepted and produced a
censoring time before the supplied hazard trajectory begins)..Rbuildignore regex bug
(^\.manuscript$, which matched nothing) so the manuscript/
development directory is now correctly excluded from the built source
tarball.sim_tte_df() now documents
explicitly that a fixed seed reproduces results for a fixed row order
of the input data, not invariant to reordering subject blocks
(standard sequential-RNG behavior, unchanged); sim_tte_ode()'s own
reproducibility contract (one U draw per idata row, insensitivity
of mrgsolve::mvgauss()'s BSV draw to intervening ordinary RNG draws)
is documented the same way.sim_tte_ode(): combining covariates/beta (or formula) with a
caller-supplied dosing data no longer corrupts the simulation.
Every dosing row's lp was previously left NA by the internal
dplyr::bind_rows(data, cov_rows) merge (only the covariate-update
rows had an lp column) -- not merely the mrgsolve warning this
produced ("Parameter column lp must not contain missing values"):
mrgsolve does not carry a NA $PARAM value forward, so once lp
went NA at a dosing record, the p11 survival state became, and
permanently stayed, NaN for the rest of that subject's trajectory
-- silently turning a real later event into administrative censoring,
with no error or warning identifying which subjects were affected.
The merge now fills every dosing row's lp by last-observation-
carried-forward against that subject's own covariate trajectory
before combining the two, and sorts the merged data by ID/time
(also fixing two related failure modes from the same unsorted merge:
mrgsolve erroring "the data set is not sorted by time", or silently
simulating a subject's rows as two disjoint blocks). A data frame
that already has its own lp column now errors clearly instead of
silently colliding with the one covariates/beta/formula builds.
This bug was only ever reachable through covariates/beta/
formula combined with data in sim_tte_ode(), both new in this
(still unreleased) 1.1.0 -- no CRAN-released behavior is affected.
Any local 1.1.0-development result produced by combining
covariates/beta/formula with a custom dosing data should be
re-run; every other sim_tte_ode()/sim_tte() call path is
untouched. Found and reported by the simttepower companion
package's own integration testing.sim_tte_ode(): a dosing data frame whose unique IDs don't
exactly match the resolved subject count (n/idata) now errors
immediately, naming both numbers, instead of silently dropping
subjects (n too large -- no warning) or failing with mrgsolve's
own opaque "ID found in the data set, but not in idata" (n too
small). data supplied without an explicit n/idata now infers
the subject count from length(unique(data$ID)) directly, rather
than defaulting to a single subject. data with no ID column at
all is likewise now an error whenever more than one subject is
resolved (previously also silently dropped subjects). New in this
(still unreleased) 1.1.0; no CRAN-released behavior is affected.
Found and reported by the simttepower companion package's own
testing.tte_model()-converted models now survive a genuinely separate
process. A model built in one R process and used in a different one
(a chunked/array-job task reading a saved tte_model() object back
from disk, with no still-running parent process) previously failed
with mrgsolve's own "[loadso] the model dll file doesn't exist"
-- the converted model's compiled shared object lived under the
building process's own cache, gone once that process exited.
simtte_prepare_model() now rebuilds a converted model from its own
$code/$name, into the current process's own
simtte_model_cache(), whenever the object it was given does not
resolve there -- deterministic, and never a global mrgsolve option,
so correctness never depends on the two processes sharing a
filesystem. tte_model() itself now also builds into
simtte_model_cache() (previously mrgsolve's own default), so a
persistent simtte.cache_dir shared across processes can turn this
into a cache hit rather than a real recompile. New in this (still
unreleased) 1.1.0; no CRAN-released behavior is affected. Found and
reported by the simttepower companion package's own testing.sim_tte_ode_models(): lists every built-in library model name
accepted by sim_tte_ode(model = ...), together with each model's
valid omega targets -- for building a scenario over model
programmatically instead of hardcoding the name list. Mirrors the
existing simtte_example_models().draw_censoring_times(spec, n, seed = NULL): draws directly from
an add_censoring()-shaped distribution spec, with no events data
frame required -- the same dispatcher add_censoring() and
visit_schedule_informative()'s extra_visit_after_event already
share internally, now usable standalone.covariates frame with
no time and no ID column now recycles a single row to every
subject (a population-level constant) or, with more than one row,
defines the subject count directly (n inferred when left at its
default, checked for an exact match otherwise). A formula with no
intercept that expands a factor to all its levels (R's own
model.matrix() convention for the first such factor) now names it
in a message().simtte_prepare_model(model, ...) builds (or reuses an already-built)
and loadso()s a model once, returning an object sim_tte_ode()
never rebuilds -- call it once per parallel::makePSOCKcluster()
worker at cluster initialization (parallel::clusterCall(cl, simtte::simtte_prepare_model, model = ...)), then pass the same
model = value to every later sim_tte_ode() call on that worker.
sim_tte_ode() uses the same function internally for every model
it is given, character name or tte_model()-converted alike -- there
is exactly one model-loading path, whether or not a caller prepares
anything ahead of time, and every path now calls
mrgsolve::loadso() unconditionally (previously only relevant, and
absent, for a worker scenario at all). simtte_model_cache() reports
(and, by default, session-scopes under tempdir()) where a compiled
model is built and stored; set options(simtte.cache_dir = tools::R_user_dir("simtte", "cache")) to persist it across R
sessions. See ?sim_tte_ode "Parallel simulation" for the intended
cluster-init pattern. A naive shared-cache-directory approach was
found to race intermittently -- reproduced directly, 4/5 and 6/15
failures under two different concurrent-build/reconstruct patterns
-- which this design avoids by construction, not a retry.
For model = "mspline", simtte_prepare_model() takes only knots
(only its length -- which shipped variant to compile -- matters at
prepare time); coefs/boundary_knots are not parameters of this
function and error, naming the argument, if supplied.simtte_model_cache_clear(all = FALSE): removes compiled model
artifacts built under simtte_model_cache() -- by default just the
current R/simtte/mrgsolve version's subdirectory, or every version
subdirectory with all = TRUE (for a persistent simtte.cache_dir
that has accumulated subdirectories across upgrades). Only ever
removes directories this package itself created; never the configured
base directory itself.sim_tte()'s own input validation uses
call. = FALSE consistently with every other exported function;
?sim_tte_ode reads in one coherent order (mechanism, models,
inputs, BSV, censoring, reproducibility); every shipped ODE library
model's survival scaffold is now verified byte-identical by a
permanent test; inst/WORDLIST added for devtools::spell_check().
No exported function's behavior changed as a result (verified against
the backward-compatibility audit and the full test suite).sim_tte_df(): time is now resolved by
column name, not position..get_tte() to use the conventional <= U inverse-transform
boundary rule.time_var argument to sim_tte_df().mrgsolve ODE systems), rather than Weibull and spline
models specifically.\value tag to pipe.Rd documenting the return value of the pipe operator.\donttest{} so they run during automated checks; examples now execute in under 5 seconds.sim_tte(), sim_tte_df(), explore_pi_tq_surv().ms_data).