simtte is an R package for simulating time-to-event (survival)
datasets whose hazard is driven by a joint
pharmacokinetic/pharmacodynamic (PK/PD) model, using
mrgsolve as the ODE
solver backend. The survival probability is integrated as one more ODE
compartment alongside the PK/PD model’s own states, and each subject’s
event time is located during integration (in-solver event detection),
not resolved after the fact from a pre-simulated trajectory. Six
built-in PK/PD hazard models ship out of the box, and tte_model()
converts any user-supplied mrgsolve model into one usable the same
way. Covariates (including a formula/design-matrix interface),
between-subject variability, right/interval censoring, and realistic
visit schedules all compose with any model. The original closed-form
Weibull/M-spline API (sim_tte()/sim_tte_df()) remains fully
supported for the cases it covers better (an arbitrary M-spline knot
count, exact Weibull behavior for very small shapes, or an externally
simulated survival trajectory from any source).
A companion package, simttepower (forthcoming), will build trial replication and power-analysis tooling on top of simtte.
simtte addresses two related simulation needs:
sim_tte_ode()): a
hazard driven by drug concentration, a PD response, or a
target-mediated disposition quantity, integrated directly as part of
the ODE system — either one of six built-in library models, or your
own mrgsolve model converted with tte_model(). Supports dosing,
covariates (raw columns, or a formula built into a design matrix),
and between-subject variability (omega). add_censoring()/
add_interval_censoring() and a family of visit-schedule generators
layer right and interval censoring, with realistic missed-visit,
dropout, and outcome-reactive assessment timing, on top of any
simulated output.sim_tte()/
sim_tte_df(), the original API): a unified interface for Weibull and
flexible M-spline baseline hazards, plus a fully model-agnostic entry
point (sim_tte_df()) for any already-simulated (ID, time, p11)
trajectory. Event times are generated by inverse transform sampling
(Bender et al., 2005), with an optional sub-grid refinement
(event_time_method = "log_survival").sim_tte_ode(model = "mspline") plus
covariates; see ?sim_tte_ode, “Simulating from a Cox model”, and the
worked example in vignette("pkpd-time-to-event")).install.packages("simtte")
simtte depends on mrgsolve, which requires a working C++ compiler. Install the appropriate toolchain for your platform before proceeding:
| Platform | Requirement |
|---|---|
| Windows | Rtools |
| macOS | Xcode command line tools: xcode-select --install
|
| Linux |
g++ via your package manager (e.g., apt install g++) |
# install.packages("devtools")
# devtools::install_github("csetraynor/simtte")
install.packages("simtte", repos = c('https://csetraynor.r-universe.dev', 'https://cloud.r-project.org'))
One short example per capability – see the vignettes for the full picture.
library(simtte)
dose <- data.frame(ID = 1:50, time = 0, cmt = 1, amt = 100, evid = 1)
sim <- sim_tte_ode(model = "irm1_hazard", param = list(H0 = 0.01, beta_r = 1),
n = 50, end = 30, delta = 2, data = dose, seed = 1)
#> Building irm1_hazard ... done.
head(sim$events)
#> ID sim_time sim_status sim_reason
#> 1 1 30.00000 0 administrative
#> 2 2 30.00000 0 administrative
#> 3 3 30.00000 0 administrative
#> 4 4 16.40492 1 event
#> 5 5 30.00000 0 administrative
#> 6 6 18.15148 1 event
tte_model()
my_model <- "
$PARAM CL = 1, V = 20, KA = 1
$CMT DEPOT CENT
$ODE
dxdt_DEPOT = -KA*DEPOT;
dxdt_CENT = KA*DEPOT - (CL/V)*CENT;
"
tm <- tte_model(my_model, hazard = "H0 * exp(lp + beta * (CENT/V))",
params = list(H0 = 0.01, beta = 0.05))
#> Building simtte_tte_model_7b992c4fe49b ... done.
sim_tm <- sim_tte_ode(model = tm, n = 50, end = 30, delta = 2, data = dose,
seed = 1)
head(sim_tm$events)
#> ID sim_time sim_status sim_reason
#> 1 1 30.000000 0 administrative
#> 2 2 30.000000 0 administrative
#> 3 3 30.000000 0 administrative
#> 4 4 8.003627 1 event
#> 5 5 30.000000 0 administrative
#> 6 6 8.918469 1 event
baseline <- data.frame(age = rnorm(50, 50, 10),
arm = factor(rep(c("control", "treated"), 25)))
sim_cov <- sim_tte_ode(model = "weibull", param = list(mu = -1, shape = 1.5),
n = 50, end = 20, delta = 2, covariates = baseline,
formula = ~ age + arm, beta = c(age = 0.01, armtreated = -0.5), seed = 1)
head(sim_cov$events)
#> ID sim_time sim_status sim_reason
#> 1 1 1.6907000 1 event
#> 2 2 1.9528771 1 event
#> 3 3 1.0421564 1 event
#> 4 4 0.4222671 1 event
#> 5 5 1.8574621 1 event
#> 6 6 0.4012701 1 event
censored <- add_censoring(sim$events, censoring = list(dist = "exponential",
rate = 0.02), end = 30, seed = 2)
table(censored$sim_reason)
#>
#> administrative censored event
#> 29 16 5
sched <- visit_schedule(n = 50, every = 4, end = 30, jitter = 1, seed = 1)
realized <- thin_visits(sched, p_miss = 0.1, seed = 2)
interval <- add_interval_censoring(censored, visits = realized)
head(interval[, c("sim_time", "sim_status", "sim_time_left", "sim_time_right")])
#> sim_time sim_status sim_time_left sim_time_right
#> 1 30.000000 0 28.889351 Inf
#> 2 20.237404 0 19.353114 Inf
#> 3 7.332633 0 4.539683 Inf
#> 4 16.404917 1 15.534441 19.77223
#> 5 4.476309 0 0.000000 Inf
#> 6 18.151475 1 16.447422 19.82255
See vignette("pkpd-time-to-event", package = "simtte") for the full
walkthrough, vignette("bring-your-own-model", package = "simtte") for
everything tte_model() does, and
vignette("censoring-and-assessment", package = "simtte") for the full
censoring/visit-schedule toolkit.
sim_tte() / sim_tte_df()
The original closed-form Weibull/M-spline API, unchanged since CRAN 1.0.2.
set.seed(42)
lp <- matrix(rnorm(50, 0, 0.5), nrow = 50)
result <- sim_tte(
pi = lp,
mu = -1,
coefs = 1.1, # Weibull shape parameter
time = seq(0.1, 100, by = 0.1),
type = "weibull",
end_time = 100
)
#> Building weibull ... done.
head(result)
#> # A tibble: 6 × 4
#> sim_time sim_status ID lp
#> <dbl> <dbl> <dbl> <dbl>
#> 1 0.7 1 1 0.685
#> 2 4.8 1 2 -0.282
#> 3 3.1 1 3 0.182
#> 4 1.8 1 4 0.316
#> 5 0.2 1 5 0.202
#> 6 0.2 1 6 -0.0531
data("ms_data")
lp <- matrix(runif(nrow(ms_data$basis)), nrow = nrow(ms_data$basis))
result <- sim_tte(
pi = lp,
mu = ms_data$mu,
basis = ms_data$basis,
coefs = ms_data$coefs,
time = ms_data$time,
type = "ms"
)
#> Building ms ... done.
head(result)
#> # A tibble: 6 × 4
#> sim_time sim_status ID lp
#> <dbl> <dbl> <dbl> <dbl>
#> 1 6.73 0 1 0.719
#> 2 4.35 1 2 0.324
#> 3 2.86 1 3 0.779
#> 4 1.84 1 4 0.394
#> 5 1.68 1 5 0.679
#> 6 2.84 1 6 0.776
By default, sim_tte() and sim_tte_df() resolve each event time as
the first reported time at which survival falls to or below a sampled
uniform draw (event_time_method = "grid"). Setting
event_time_method = "log_survival" refines a crossing that falls
strictly between two reported time points, by linearly interpolating
cumulative hazard between them, without changing which subjects are
classified as events versus censored:
result_grid <- sim_tte(
pi = lp,
mu = -1,
coefs = 1.1,
time = seq(0.1, 100, by = 0.1),
type = "weibull",
end_time = 100,
event_time_method = "grid" # default
)
#> Loading model from cache.
result_interp <- sim_tte(
pi = lp,
mu = -1,
coefs = 1.1,
time = seq(0.1, 100, by = 0.1),
type = "weibull",
end_time = 100,
event_time_method = "log_survival" # sub-grid refinement
)
#> Loading model from cache.
See Statistical Background below for what
"log_survival" assumes and when it is exact rather than approximate.
sim_tte()/sim_tte_df() also come with explore_pi_tq_surv() (a
prognostic-index/survival-difference exploration utility) and further
Weibull/M-spline examples – see vignette("advanced-usage") for the
full walkthrough.
The Weibull hazard at time $t$ for individual $i$ is:
$$h_i(t) = \exp(\mu + \mathbf{x}_i'\boldsymbol{\beta}) \cdot \gamma \cdot t^{\gamma - 1}$$
where $\mu$ is the intercept, $\mathbf{x}_i'\boldsymbol{\beta}$ is the
linear predictor (prognostic index), and $\gamma > 0$ is the shape
parameter (coefs). The corresponding survival probability
$S_i(t) = \exp[-\exp(\mu + \mathbf{x}_i'\boldsymbol{\beta}) \cdot t^\gamma]$
has a closed form, so simtte evaluates it directly on the reported
time grid rather than by numerically integrating the survival ODE; this
is numerically robust across shape and parameter regimes (including near
$t = 0$ for $\gamma < 1$, and large intercept/linear-predictor values),
but it is specific to the Weibull model, not a general continuous-time
event-root solver.
The baseline hazard is expressed as a linear combination of M-spline basis functions:
$$h_0(t) = \sum_{k} \alpha_k M_k(t)$$
This approach (Royston & Parmar, 2002) accommodates non-monotone and
non-proportional hazard shapes, making it suitable for modelling
immunotherapy and other treatments with delayed effects. The hazard is
supplied to mrgsolve as a time-varying input evaluated at the
elements of time; between two consecutive supplied time points, the
hazard value at the earlier point applies (a
last-observation-carried-forward convention), giving a
piecewise-constant hazard on the reported grid rather than continuous
re-evaluation of the M-spline basis at arbitrary times.
The two engines resolve event times in genuinely different ways – this section describes each in turn.
sim_tte_ode() (default engine for PK/PD-linked and ODE models)Survival is one more ODE compartment, p11, alongside the model’s own
PK/PD states, with dxdt_p11 = -p11 * HAZ. A uniform draw
$U \sim \text{Uniform}(0,1)$ is made per subject up front (one
stats::runif() draw per row of idata, before the solver runs, not
after a trajectory exists), and the solver is watched internally for the
first step at which $p11(t) \le U$ – an in-solver root, latched at the
solver’s own internal step grid, which is generally finer than, and
independent of, the requested output grid (delta/add). Once a
crossing is latched, the reported event time is refined further inside
the one solver step that bracketed it (not between two reported
trajectory rows), so refinement accuracy does not depend on how coarse
delta/add are. A subject with no crossing before end is
administratively censored at end. There is no event_time_method
argument here – that is a sim_tte()/sim_tte_df()-only option (see
below). See ?sim_tte_ode’s “Event-time refinement” section for the
full mechanics and vignette("pkpd-time-to-event") for a walkthrough.
sim_tte() / sim_tte_df() (legacy 1.0.x engine)For each individual, the package obtains $S(t)$ on the exact grid
supplied via time (default seq(0, 100, by = 1), extended to
end_time if necessary, and reported by mrgsolve at exactly those
times via its tgrid mechanism — not mrgsolve’s own default output
schedule), then draws $U \sim \text{Uniform}(0,1)$ after the
trajectory exists and finds the first reported grid time $t^$ such
that $S(t^) \le U$. For sim_tte(), a subject with no such $t^*$ is
administratively censored at end_time; sim_tte_df() instead censors
at that subject’s own last reported time (which coincides with
end_time for sim_tte() by construction, but need not for an
arbitrary user-supplied trajectory with per-subject time ranges). By
default (event_time_method = "grid"), event-time resolution is exactly
this grid-based lookup, with precision limited by the spacing of time.
Setting event_time_method = "log_survival" refines a crossing that
falls strictly between two reported grid points $(t_i, S_i)$ and
$(t_{i+1}, S_{i+1})$, by linearly interpolating cumulative hazard
$H(t) = -\log S(t)$ between them and solving for the time at which
$H(t) = -\log U$. This is equivalent to assuming the hazard is constant
over $[t_i, t_{i+1})$: for the M-spline model, whose hazard genuinely is
piecewise constant on the reported grid (see above), this recovers the
event time implied by that discretized hazard exactly; for a
continuously varying hazard (the Weibull model with $\gamma \neq 1$, or
a custom mechanistic model), it is an approximation whose accuracy
improves as time is refined. event_time_method never changes which
subjects are classified as events versus censored, and never changes the
reported time for censoring or for a crossing already present at the
first reported observation — it only provides sub-grid event-time
resolution for an interior crossing, and is not a general
continuous-time root solver. event_time_method does not exist on
sim_tte_ode(), which refines every event time via the in-solver
bracket described above regardless of the output grid.
sim_tte_df() accepts a user-supplied survival trajectory (e.g. from a
custom mrgsolve model) subject to a validated contract: time
values must be finite, non-negative, and strictly increasing per subject
(duplicate or unsorted times are rejected, not silently repaired);
survival values are normalized to $[0, 1]$ (values within a small
floating-point tolerance of the boundary are clamped, values further
outside are rejected) and must be non-increasing per subject within the
same tolerance; and subjects may have different reported time ranges,
each subject being censored at their own final reported time if no
crossing occurs.
| Function | Description |
|---|---|
sim_tte_ode() |
In-solver joint PK/PD and time-to-event simulation (built-in library models or a converted user model) |
sim_tte_ode_models() |
List the built-in library model names and their between-subject-variability targets |
tte_model() |
Convert a user-supplied mrgsolve PK/PD model for use with sim_tte_ode()
|
add_censoring() |
Apply independent right censoring to any simulated events data frame |
censoring_rate_for() |
Solve for a censoring-distribution parameter hitting a target censoring fraction |
draw_censoring_times() |
Draw directly from a censoring/delay distribution spec |
add_interval_censoring() |
Map a (possibly right-censored) outcome onto a visit-schedule interval |
visit_schedule() |
Generate a per-subject visit schedule (fixed spacing + jitter) |
thin_visits() |
Missed visits and assessment dropout on a schedule (non-informative) |
visit_schedule_informative() |
A visit schedule that reacts to the simulated event time (informative) |
sim_tte() |
Simulate a survival dataset from Weibull or M-spline model (legacy API) |
sim_tte_df() |
Apply inverse transform sampling to custom mrgsolve output (legacy API) |
explore_pi_tq_surv() |
Explore survival differences across prognostic index values |
simtte_example_model() / simtte_example_models()
|
Bundled example PK/PD-hazard models for sim_tte_df()
|
ms_data |
Bundled example M-spline basis, coefficients, and time grid |
sim_tte_ode() returns a list whose $events is a data frame with
ID, sim_time, sim_status (1 = event, 0 = censored), and
sim_reason ("event"/"censored"/"administrative"), plus interval
bounds if a visit schedule was used. The legacy
sim_tte()/sim_tte_df() functions return a plain data frame:
| Column | Type | Description |
|---|---|---|
sim_time |
numeric | Simulated event or censoring time |
sim_status |
integer | Event indicator: 1 = event, 0 = censored |
ID |
integer | Subject identifier |
lp |
numeric | Log prognostic index (linear predictor) |
Four vignettes provide deeper guidance:
tte_model() conversion mechanics,
$OMEGA coexistence, and the covariate/hazard-expression boundary.vignette("pkpd-time-to-event", package = "simtte")
vignette("bring-your-own-model", package = "simtte")
vignette("censoring-and-assessment", package = "simtte")
vignette("introduction", package = "simtte")
vignette("advanced-usage", package = "simtte")
If you use simtte in published research, please cite:
Traynor C (2026). simtte: Simulate Bespoke Time-to-Event Models Using
ODEs. R package version 1.1.0.
https://CRAN.R-project.org/package=simtte
Bender R, Augustin T, Blettner M (2005). Generating survival times to simulate Cox proportional hazards models. Statistics in Medicine, 24(11), 1713–1723. doi:10.1002/sim.2059
Royston P, Parmar MKB (2002). Flexible parametric proportional-hazards and proportional-odds models for censored survival data, with application to prognostic modelling and estimation of treatment effects. Statistics in Medicine, 21(15), 2175–2197. doi:10.1002/sim.1203
GPL (>= 2)