@@ -257,9 +257,7 @@ If all three gates pass, the analysis function is called on the filtered
257257data 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
274272cond_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)
367365tm $ 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
404402and what to compute. Understanding how they are stored and evaluated is
405403key 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(
490463trial $ 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)
506477trial $ 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(
522508trial $ 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
551540my_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
634637analysis_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 (
0 commit comments