Two API Styles: Direct and Generator
api-styles.RmdIntroduction
rxsim supports two equivalent ways to build the same trial.
-
Style A: Direct instantiation builds
Population,Timer,Condition, andTrialobjects yourself. -
Style B: Generator API gives
replicate_trial()the population and analysis generators, then lets rxsim build eachTrialfor you.
The two styles are equivalent when the design itself is the same: same arms, endpoint distributions, enrollment, dropout, and trigger logic. The difference is where you want control.
Prefer the direct style when you want to inspect or edit the
Timer, work with fixed schedules, or debug one trial step
by step. Prefer the generator style when you want many stochastic
replicates, quick scenario grids, or minimal boilerplate.
The sections below use the same two-arm design in both styles:
- placebo (
pbo) versus treatment (trt) -
sample_size = 40withallocation = c(1, 1) - stochastic enrollment from
rexp(n, rate = 1) - stochastic dropout from
rexp(n, rate = 0.05) - continuous endpoint with mean
0for placebo and0.5for treatment - one final analysis at full enrollment
-
10replicates for the operating characteristics
sample_size <- 40L
arms <- c("pbo", "trt")
allocation <- c(1, 1)
delta <- 0.5
n_reps <- 10L
enrollment <- function(n) rexp(n, rate = 1)
dropout <- function(n) rexp(n, rate = 0.05)
make_outcome_data <- function(n, mean_shift) {
data.frame(
id = seq_len(n),
outcome = rnorm(n, mean = mean_shift, sd = 1),
readout_time = 0,
stringsAsFactors = FALSE
)
}
final_analysis <- function(df, current_time) {
enrolled <- subset(df, !is.na(enroll_time))
fit <- stats::t.test(outcome ~ arm, data = enrolled)
data.frame(
n = nrow(enrolled),
mean_pbo = mean(enrolled$outcome[enrolled$arm == "pbo"]),
mean_trt = mean(enrolled$outcome[enrolled$arm == "trt"]),
p_value = unname(fit$p.value),
stringsAsFactors = FALSE
)
}
make_final_condition <- function() {
Condition$new(
where = enroll_trigger(1.0, sample_size),
analysis = final_analysis,
name = "final"
)
}Style A: Direct instantiation
Use the direct style when you want to see every moving part. You create the plan, build the timer, size each arm, define the trigger, and assemble the trial yourself.
# 1. Draw one stochastic enrollment and dropout plan.
direct_plan <- stochastic_schedule(
sample_size = sample_size,
arms = arms,
allocation = allocation,
enrollment = enrollment,
dropout = dropout
)
# 2. Build the timer from that plan.
direct_timer <- Timer$new(name = "direct_timer")
direct_timer$add_schedule(direct_plan)
# 3. Compute the planned arm sizes from the timer input.
direct_n_by_arm <- vapply(
arms,
function(arm_name) {
as.integer(sum(direct_plan$enroll[direct_plan$arm == arm_name]))
},
integer(1)
)
# 4. Instantiate each arm population explicitly.
pop_pbo <- Population$new(
name = "pbo",
data = make_outcome_data(direct_n_by_arm[["pbo"]], mean_shift = 0)
)
pop_trt <- Population$new(
name = "trt",
data = make_outcome_data(direct_n_by_arm[["trt"]], mean_shift = delta)
)
# 5. Define the final analysis with a condition object.
direct_final <- make_final_condition()
# 6. Assemble the Trial object.
direct_trial <- Trial$new(
name = "direct_trial",
timer = direct_timer,
population = list(pop_pbo, pop_trt),
conditions = list(direct_final)
)In this style, the timer is a first-class object. You can inspect the time grid, modify individual timepoints, or replace the whole schedule before running.
head(direct_plan)
#> time arm enroll drop
#> 1 0.1983368 pbo 1 0
#> 2 1.1427231 pbo 1 0
#> 3 1.1809150 pbo 1 0
#> 4 1.6540916 pbo 1 0
#> 5 3.4317033 pbo 1 0
#> 6 5.0334307 pbo 1 0
direct_timer$get_end_timepoint()
#> [1] 764.053
# 7. Run the single direct trial.
direct_trial$run()
collect_results(direct_trial)
#> replicate timepoint analysis n mean_pbo mean_trt p_value
#> 1 1 39.32605 final 40 -0.0557219 0.1194667 0.6202483For many stochastic replicates, the direct pattern is usually wrapped in a small constructor function.
build_direct_trial <- function(name) {
# 1. Generate a fresh stochastic plan for this replicate.
plan <- stochastic_schedule(
sample_size = sample_size,
arms = arms,
allocation = allocation,
enrollment = enrollment,
dropout = dropout
)
# 2. Turn that plan into a timer.
timer <- Timer$new(name = paste0(name, "_timer"))
timer$add_schedule(plan)
# 3. Size each arm from the generated plan.
n_by_arm <- vapply(
arms,
function(arm_name) {
as.integer(sum(plan$enroll[plan$arm == arm_name], na.rm = TRUE))
},
integer(1)
)
# 4. Instantiate populations explicitly.
populations <- list(
Population$new(
name = "pbo",
data = make_outcome_data(n_by_arm[["pbo"]], mean_shift = 0)
),
Population$new(
name = "trt",
data = make_outcome_data(n_by_arm[["trt"]], mean_shift = delta)
)
)
# 5. Create a fresh condition object for this replicate.
conditions <- list(make_final_condition())
# 6. Return the assembled trial.
Trial$new(
name = name,
timer = timer,
population = populations,
conditions = conditions
)
}
direct_trials <- lapply(seq_len(n_reps), function(i) {
build_direct_trial(paste0("direct_", i))
})
run_trials(direct_trials)
direct_results <- collect_results(direct_trials)
head(direct_results)
#> replicate timepoint analysis n mean_pbo mean_trt p_value
#> 1 1 45.97723 final 40 0.22031537 0.6831231 0.11616961
#> 2 2 38.68791 final 40 -0.12834383 0.6033173 0.03610186
#> 3 3 39.78981 final 40 0.17085927 0.6229233 0.13717783
#> 4 4 40.73727 final 40 -0.07649800 0.3610610 0.12120162
#> 5 5 42.68275 final 40 0.02060517 0.3789782 0.29638352
#> 6 6 44.00494 final 40 0.24331892 0.3731780 0.71147851Style B: Generator API
Use the generator API when you want the same design with less setup.
You define functions for the arm populations and analyses, then let
replicate_trial() build the timers, conditions, and trials
internally.
# 1. Define one population generator per arm.
population_generators <- list(
pbo = function(n) make_outcome_data(n, mean_shift = 0),
trt = function(n) make_outcome_data(n, mean_shift = delta)
)
# 2. Define the analysis generator with a condition object.
analysis_generators <- list(
final = list(
trigger = enroll_trigger(1.0, sample_size),
analysis = final_analysis
)
)The trigger must be a trigger object such as
enroll_trigger(1.0, sample_size). No quotation helpers are
needed.
# 3. Ask rxsim to build 10 independent trials.
generator_trials <- replicate_trial(
trial_name = "generator",
sample_size = sample_size,
arms = arms,
allocation = allocation,
enrollment = enrollment,
dropout = dropout,
analysis_generators = analysis_generators,
population_generators = population_generators,
n = n_reps
)
# 4. Run all generated trials.
run_trials(generator_trials)
generator_results <- collect_results(generator_trials)
head(generator_results)
#> replicate timepoint analysis n mean_pbo mean_trt p_value
#> 1 1 41.31080 final 40 0.14790964 0.64055125 0.1843726111
#> 2 2 42.57187 final 40 -0.08040526 0.46229123 0.0748187763
#> 3 3 36.74258 final 40 -0.37301982 0.44163311 0.0074422896
#> 4 4 32.38417 final 40 -0.03727242 0.04886914 0.8133832280
#> 5 5 44.70138 final 40 -0.48426483 0.48936266 0.0175744028
#> 6 6 41.63317 final 40 -0.35693944 0.80935517 0.0004009703This style is shorter because replicate_trial() handles
the timer construction, population sizing, and condition creation for
every replicate.
When to use Style A
Prefer direct instantiation when you want to work directly with the underlying objects.
- Manually manipulate the
Timerbefore running. - Use deterministic schedules from
deterministic_schedule()or a hand-built plan. - Debug a single trial by inspecting
timer,population, andlocked_data. - Build multi-stage designs where the schedule is easier to express directly, such as the seamless pattern in Example 5.
When to use Style B
Prefer the generator API when the design follows the standard
workflow that replicate_trial() already knows how to
build.
- Run scenario grids generated with
expand_grid(). - Simulate standard stochastic enrollment and dropout with minimal setup.
- Write scripts that need many replicates and little boilerplate.
- Keep design changes local to a few small generator functions.
Mixing the styles
A common workflow is to start with the direct style for one trial,
inspect it, then scale out with run_trials() once you are
happy with the setup.
# 1. Build one trial directly so you can inspect it.
mixed_trial <- build_direct_trial("mixed_1")
mixed_trial$timer$timelist
# 2. Extend the same direct pattern to more replicates.
mixed_trials <- c(
list(mixed_trial),
lapply(2:5, function(i) build_direct_trial(paste0("mixed_", i)))
)
# 3. Run them together with the usual batch helper.
run_trials(mixed_trials)If you intentionally want the same fixed timer and populations in
each replicate, clone_trial() is also useful. That pattern
is most natural for deterministic schedules, not for stochastic
enrollment.
# Deep copies with independent timer and population state.
clones <- clone_trial(mixed_trial, n = 2)
names(clones)
#> NULLNext steps
- Add interims and compose trigger objects: Conditions and Triggers
- Inspect the
Trialclass documentation: Trial reference