Skip to content

Commit a5094cc

Browse files
committed
Deploying to gh-pages from @ 67222cb 🚀
1 parent 27833e1 commit a5094cc

65 files changed

Lines changed: 726 additions & 384 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

404.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

articles/concepts.html

Lines changed: 90 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

articles/concepts.md

Lines changed: 84 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,7 @@ If all three gates pass, the analysis function is called on the filtered
257257
data and the result is stored. Trigger state (`trigger_count`,
258258
`last_trigger_time`) is updated automatically.
259259

260-
Construct a `Condition` with
261-
[`rlang::quos()`](https://rlang.r-lib.org/reference/defusing-advanced.html)
262-
to capture the filter predicates:
260+
Build a `Condition` from a validated trigger helper:
263261

264262
``` r
265263
# A toy snapshot data frame
@@ -272,7 +270,7 @@ snapshot <- data.frame(
272270

273271
# Fire when at least 4 subjects are enrolled
274272
cond_interim <- Condition$new(
275-
where = rlang::quos(sum(!is.na(enroll_time)) >= 4),
273+
where = enroll_trigger(1.0, 4),
276274
analysis = function(df, current_time) {
277275
data.frame(n_enrolled = sum(!is.na(df$enroll_time)),
278276
fired_at = current_time)
@@ -367,8 +365,8 @@ tm$add_timepoint(time = 15, arm = "A", enroller = 0L, dropper = 0L)
367365
tm$add_timepoint(time = 15, arm = "B", enroller = 0L, dropper = 0L)
368366

369367
# Trigger: fire at calendar time 15
370-
cond_final <- Condition$new(
371-
where = rlang::quos(.data$time %in% 15),
368+
cond_final <- trigger_by_calendar(
369+
cal_time = 15,
372370
analysis = function(df, current_time) {
373371
enrolled <- subset(df, !is.na(enroll_time))
374372
data.frame(
@@ -404,61 +402,40 @@ Triggers are the mechanism by which rxsim knows when to analyse the data
404402
and what to compute. Understanding how they are stored and evaluated is
405403
key to writing correct and flexible simulation code.
406404

407-
### Why rlang::exprs()?
405+
### Safe trigger helpers
408406

409-
A trigger condition needs to be a *stored* expression, not an
410-
immediately evaluated one. When you write
411-
`sum(!is.na(enroll_time)) >= 20`, R would evaluate it at the point of
412-
definition — before any data exists.
413-
[`rlang::exprs()`](https://rlang.r-lib.org/reference/defusing-advanced.html)
414-
wraps the expression in a quoted form so it is only evaluated later,
415-
inside `check_conditions()`, against the actual snapshot:
407+
Triggers for `analysis_generators` are created with validated helper
408+
constructors rather than quoted expressions. Use
409+
[`enroll_trigger()`](https://boehringer-ingelheim.github.io/rxsim/reference/trigger_primitives.md)
410+
for enrollment milestones,
411+
[`calendar_trigger()`](https://boehringer-ingelheim.github.io/rxsim/reference/trigger_primitives.md)
412+
for calendar-time analyses, and
413+
[`value_trigger()`](https://boehringer-ingelheim.github.io/rxsim/reference/trigger_primitives.md)
414+
/
415+
[`count_trigger()`](https://boehringer-ingelheim.github.io/rxsim/reference/trigger_primitives.md)
416+
when you want to write a predicate against a specific snapshot column.
416417

417418
``` r
418-
# Stored expressions, not immediately evaluated:
419-
rlang::exprs(sum(!is.na(enroll_time)) >= 20)
419+
enroll_trigger(0.5, 100)
420+
calendar_trigger(c(12, 24))
421+
value_trigger("time", ">=", 24)
422+
count_trigger("enroll_time", ">=", 50)
420423
```
421424

422-
`Condition$new()` uses
423-
[`rlang::quos()`](https://rlang.r-lib.org/reference/defusing-advanced.html)
424-
instead of
425-
[`rlang::exprs()`](https://rlang.r-lib.org/reference/defusing-advanced.html).
426-
The difference is that `quos()` also captures the *calling environment*,
427-
so values from the surrounding scope (e.g., `target_n`) are available
428-
inside the expression when it is evaluated. Use
429-
[`rlang::quos()`](https://rlang.r-lib.org/reference/defusing-advanced.html)
430-
in `Condition$new(where = ...)` and
425+
The helpers take plain R values, so there is no need for
431426
[`rlang::exprs()`](https://rlang.r-lib.org/reference/defusing-advanced.html)
432-
when supplying triggers to
433-
[`replicate_trial()`](https://boehringer-ingelheim.github.io/rxsim/reference/replicate_trial.md)’s
434-
`analysis_generators`.
435-
436-
### The !! (bang-bang) operator
427+
or `!!` anymore. Invalid operators, wrong column names, and type
428+
mismatches are rejected immediately when the trigger is constructed.
437429

438-
When you want to inject a value from the current R environment into a
439-
stored expression, use `!!`. Without it, the variable name is treated as
440-
a column name in the snapshot data frame — almost certainly not what you
441-
want:
442-
443-
``` r
444-
target_n <- 40
445-
446-
# CORRECT: !! injects the value 40 at definition time
447-
trigger <- rlang::exprs(sum(!is.na(enroll_time)) >= !!target_n)
448-
449-
# WRONG: "target_n" would be looked for as a column name in the snapshot
450-
trigger_bad <- rlang::exprs(sum(!is.na(enroll_time)) >= target_n)
451-
```
430+
Compose helper triggers with `&` (AND) and `|` (OR), then pass the
431+
result directly as the `trigger` field in `analysis_generators`, or
432+
construct a `Condition` with `Condition$new(where = trigger, ...)`.
452433

453-
This distinction matters when you loop over scenarios with different
454-
sample sizes: each iteration should bake in its own `target_n` value via
455-
`!!`.
434+
### Columns available to trigger helpers
456435

457-
### Columns available in a trigger expression
458-
459-
The trigger expression is evaluated against the snapshot data frame,
460-
which contains all columns from the `Population`’s `data` plus the four
461-
columns appended by `Trial`:
436+
Trigger helpers are evaluated against the snapshot data frame after they
437+
are converted to a `Condition`. The snapshot contains all columns from
438+
the `Population`’s `data` plus the four columns appended by `Trial`:
462439

463440
- `enroll_time` — calendar enrollment time (`NA` if not enrolled)
464441
- `drop_time` — calendar dropout time (`NA` if not dropped)
@@ -471,17 +448,13 @@ also available.
471448

472449
### trigger_by_calendar() and trigger_by_fraction()
473450

474-
> **Note:** These helper functions are being refactored in an upcoming
475-
> release to accept a `Trial` object directly. In the meantime, use
476-
> `Condition$new()` (shown below) to build triggers.
477-
478-
`Condition$new()` with a calendar-time filter is the recommended
479-
approach for pre-planned analyses:
451+
For the two most common cases, use the convenience constructors directly
452+
to create `Condition` objects:
480453

481454
``` r
482455
# Fire at calendar time 24
483-
cond_final <- Condition$new(
484-
where = rlang::quos(.data$time %in% 24),
456+
cond_final <- trigger_by_calendar(
457+
cal_time = 24,
485458
analysis = function(df, current_time) {
486459
data.frame(n_enrolled = sum(!is.na(df$enroll_time)))
487460
},
@@ -490,27 +463,40 @@ cond_final <- Condition$new(
490463
trial$conditions <- append(trial$conditions, list(cond_final))
491464
```
492465

493-
For information-fraction-based interims, filter on enrolled count:
494-
495466
``` r
496467
# Interim at 50% enrollment (target = 100)
497-
cond_interim <- Condition$new(
498-
where = rlang::quos(sum(!is.na(enroll_time)) >= 50),
468+
cond_interim <- trigger_by_fraction(
469+
fraction = 0.5,
470+
sample_size = 100,
499471
analysis = function(df, current_time) {
500472
enrolled <- subset(df, !is.na(enroll_time))
501473
data.frame(n = nrow(enrolled), time = current_time)
502474
},
503-
name = "interim_50pct",
504-
max_triggers = 1L
475+
name = "interim_50pct"
505476
)
506477
trial$conditions <- append(trial$conditions, list(cond_interim))
507478
```
508479

480+
When you need more than one predicate, compose helpers before building
481+
the `Condition`:
482+
483+
``` r
484+
trig_both <- enroll_trigger(0.5, 100) & calendar_trigger(24)
485+
486+
cond_both <- Condition$new(
487+
where = trig_both,
488+
analysis = my_analysis,
489+
name = "interim_after_half_enrollment_at_day_24"
490+
)
491+
trial$conditions <- append(trial$conditions, list(cond_both))
492+
```
493+
509494
### Custom conditions
510495

511-
You can condition on any expression involving the snapshot columns. For
512-
time-to-event endpoints this commonly means waiting for a target number
513-
of events rather than a target enrollment count:
496+
For trigger logic that cannot yet be expressed with the safe helpers,
497+
`Condition$new(where = rlang::quos(...))` remains the advanced escape
498+
hatch. For time-to-event endpoints this commonly means waiting for a
499+
target number of events rather than a target enrollment count:
514500

515501
``` r
516502
# Fire when 30 events have been observed (event = 1, censored = 0)
@@ -522,32 +508,35 @@ cond_events <- Condition$new(
522508
trial$conditions <- append(trial$conditions, list(cond_events))
523509
```
524510

525-
Multiple predicates in `where` are ANDed together, exactly as in
526-
[`dplyr::filter()`](https://dplyr.tidyverse.org/reference/filter.html):
511+
When the logic *can* be expressed with helper predicates, prefer
512+
composing `rxsim_trigger` objects with `&` instead of supplying multiple
513+
`where` quosures:
527514

528515
``` r
529-
# Fire only once the treatment arm has 15 enrolled subjects
530-
cond_trt <- Condition$new(
531-
where = rlang::quos(
532-
arm == "treatment",
533-
sum(!is.na(enroll_time[arm == "treatment"])) >= 15
534-
),
516+
# Fire once time 12 is reached and at least 15 subjects are enrolled
517+
cond_interim <- Condition$new(
518+
where = value_trigger("time", ">=", 12) & count_trigger("enroll_time", ">=", 15),
535519
analysis = my_analysis,
536-
name = "trt_interim"
520+
name = "interim_after_time12"
537521
)
538-
trial$conditions <- append(trial$conditions, list(cond_trt))
522+
trial$conditions <- append(trial$conditions, list(cond_interim))
539523
```
540524

541525
### The analysis function signature
542526

543-
Every analysis function receives two arguments:
527+
The analysis function always receives the filtered data frame and clock
528+
time as its first two positional arguments. Scenario-specific values can
529+
be declared as additional named parameters and injected via
530+
`analysis_args`:
544531

545532
- `df`: the **full snapshot** — all arms, all currently enrolled
546533
subjects. The condition filter determined *whether* to fire; the
547534
analysis function decides *what to compute* from the full data.
548535
- `current_time`: the numeric clock time at which the condition fired.
536+
- `...`: any extra named values supplied through `analysis_args`.
549537

550538
``` r
539+
# Basic signature
551540
my_analysis <- function(df, current_time) {
552541
enrolled <- subset(df, !is.na(enroll_time))
553542
data.frame(
@@ -558,6 +547,20 @@ my_analysis <- function(df, current_time) {
558547
mean(enrolled$data[enrolled$arm == "control"])
559548
)
560549
}
550+
551+
# With extra scenario-specific arguments
552+
my_analysis_ext <- function(df, current_time, alpha, cov_matrix) {
553+
# alpha and cov_matrix are injected from analysis_args at call time
554+
enrolled <- subset(df, !is.na(enroll_time))
555+
data.frame(fired_at = current_time, n = nrow(enrolled), alpha = alpha)
556+
}
557+
558+
cond <- Condition$new(
559+
where = enroll_trigger(1.0, 100),
560+
analysis = my_analysis_ext,
561+
analysis_args = list(alpha = 0.05, cov_matrix = diag(2)),
562+
name = "final"
563+
)
561564
```
562565

563566
### Return values
@@ -633,9 +636,7 @@ dropout <- function(n) rexp(n, rate = 0.02)
633636
# Step 4 — analysis trigger: fire when full enrollment is reached
634637
analysis_generators <- list(
635638
final = list(
636-
trigger = rlang::exprs(
637-
sum(!is.na(enroll_time)) >= !!sample_size
638-
),
639+
trigger = enroll_trigger(1.0, sample_size),
639640
analysis = function(df, current_time) {
640641
enrolled <- subset(df, !is.na(enroll_time))
641642
data.frame(

articles/enrollment-dropout.html

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

articles/enrollment-dropout.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ population_generators <- list(
456456

457457
analysis_generators <- list(
458458
final = list(
459-
trigger = rlang::exprs(sum(!is.na(enroll_time)) >= 12L),
459+
trigger = enroll_trigger(1.0, 12L),
460460
analysis = function(df, current_time) {
461461
enrolled <- subset(df, !is.na(enroll_time))
462462
data.frame(
@@ -532,8 +532,8 @@ add_timepoints(tmr3, fixed_plan)
532532

533533
# Condition: fire at the final calendar time
534534
final_t <- tmr3$get_end_timepoint()
535-
cond_final <- Condition$new(
536-
where = rlang::quos(.data$time %in% !!final_t),
535+
cond_final <- trigger_by_calendar(
536+
cal_time = final_t,
537537
analysis = function(df, current_time) {
538538
enrolled <- subset(df, !is.na(enroll_time))
539539
data.frame(n_enrolled = nrow(enrolled), time = current_time)

0 commit comments

Comments
 (0)