From c1065ea8c197df75953f6f552a2f568b6157f2c9 Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 09:02:39 -0400 Subject: [PATCH 01/23] Fix seasonal exact binomial simulation updates --- DESCRIPTION | 2 +- NEWS.md | 14 ++ R/simBinomialSeasonalExact.R | 206 +++++++++++++----- R/toInteger.R | 39 +++- man/simBinomialSeasonalExact.Rd | 10 +- man/toInteger.Rd | 9 +- .../testthat/test-developer-test-toInteger.R | 14 +- ...ndependent-test-simBinomialSeasonalExact.R | 70 ++++++ .../test-independent-test-toBinomialExact.R | 8 +- .../test-independent-test-toInteger.R | 36 ++- vignettes/SurvivalOverview.Rmd | 2 +- vignettes/VaccineEfficacy.Rmd | 2 +- vignettes/toInteger.Rmd | 2 +- 13 files changed, 318 insertions(+), 96 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 61e8841e..09e9b1c7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: gsDesign -Version: 3.9.1 +Version: 3.9.1.9000 Title: Group Sequential Design Authors@R: c( person("Keaven", "Anderson", email = "keaven_anderson@merck.com", role = c("aut", "cre")), diff --git a/NEWS.md b/NEWS.md index 3e5ced6d..5e075cb5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -65,11 +65,22 @@ ## Bug fixes +- `simBinomialSeasonalExact()` now stops simulated trials at the first + efficacy or futility boundary crossing for reporting stopping time, total + events, and total enrollment, while preserving the non-binding futility + convention for efficacy crossing probability. The simulation also updates + exact-binomial bounds within each trial using the observed total event counts + and defaults fixed per-season enrollment to the design's planned seasonal + enrollment (#264). - `toInteger()` now preserves selective-bound flags (`testUpper`, `testLower`, `testHarm`) and harm-bound spending (`sfharm`, `sfharmparam` for `test.type` 7 or 8) when recomputing the design after integer sample size or event-count rounding. Previously the internal `gsDesign()` call omitted these settings, so inactive looks could incorrectly become active. +- `toInteger()` now rounds survival-design event counts down while still + rounding enrollment to the requested allocation multiple. A warning is issued + when a rounded-up event count is not achievable under the given enrollment + model, avoiding rare-event failures with equal allocation (#264). - Fixed sign inconsistency in `hrn2z()` which used `sign(hr0 - hr1)` while `zn2hr()` used `sign(hr1 - hr0)`, preventing correct round-trip conversion. Both now use `sign(hr1 - hr0)` (@keaven, #251). @@ -123,6 +134,9 @@ - Added regression tests for `toBinomialExact()` `usTime`/`lsTime` overrides and selective-futility behavior, plus tests for new futility stopping summary outputs from `simBinomialSeasonalExact()`. +- Added regression tests for `simBinomialSeasonalExact()` stopping summaries, + design-based fixed enrollment defaults, and the rare-event `toInteger()` + equal-allocation path (#264). # gsDesign 3.9.0 (February 2026) diff --git a/R/simBinomialSeasonalExact.R b/R/simBinomialSeasonalExact.R index 20659218..83ac0212 100644 --- a/R/simBinomialSeasonalExact.R +++ b/R/simBinomialSeasonalExact.R @@ -26,11 +26,11 @@ #' @param timing Optional increasing cumulative spending-time vector ending at 1 #' used to derive `planned_counts` when `planned_counts = NULL`. #' @param enroll_control_per_look Optional control-arm enrollment by look -#' (scalar or length `k` integer vector). If `NULL`, this is calibrated -#' from `planned_counts` under the largest value of `ve`. +#' (scalar or length `k` integer vector). If both enrollment vectors are +#' `NULL`, defaults are derived from the seasonal accrual pattern in `gsD`. #' @param enroll_experimental_per_look Optional experimental-arm enrollment by -#' look (scalar or length `k` integer vector). If `NULL`, this is set using -#' `gsD$ratio` from `enroll_control_per_look`. +#' look (scalar or length `k` integer vector). If `NULL` and +#' `enroll_control_per_look` is supplied, this is set using `gsD$ratio`. #' @param adaptive Logical vector specifying whether to simulate fixed and/or #' adaptive enrollment scenarios. #' @param adapt_looks Integer vector of look indices after which adaptation can @@ -39,7 +39,7 @@ #' when adaptation is enabled. #' @param usTime Optional upper spending-time override passed to #' [toBinomialExact()]. If `NULL`, spending time defaults to -#' `planned_counts / planned_final_events` (capped at 1). +#' `1 / k, 2 / k, ..., 1`. #' @param lsTime Optional lower spending-time override for `test.type = 4`. #' If `NULL`, this defaults to `usTime`. #' @param final_full_spending Logical scalar. If `TRUE`, force full alpha @@ -193,7 +193,7 @@ simBinomialSeasonalExact <- function( adapt_looks <- sort(unique(as.integer(adapt_looks))) planned_final_events <- max(planned_counts) - default_spending <- pmin(planned_counts / planned_final_events, 1) + default_spending <- seq_len(k) / k usTime_resolved <- normalizeSpendingTimeVector(usTime, default_spending, k, "usTime") lsTime_resolved <- normalizeSpendingTimeVector(lsTime, usTime_resolved, k, "lsTime") if (isTRUE(final_full_spending)) { @@ -218,24 +218,36 @@ simBinomialSeasonalExact <- function( ) ) - if (is.null(enroll_control_per_look)) { - ve_cal <- max(ve) - lambda_c_cal <- -log(1 - control_event_rate[which.max(ve)]) / season_length - eta_cal <- if (dropout_rate == 0) 0 else -log(1 - dropout_rate) / season_length - probs_cal <- seasonalBinomialProb(ve = ve_cal, lambda_c = lambda_c_cal, eta = eta_cal, season_length = season_length) - m <- rev(seq_len(k)) - sum_c <- sum(expectedEventsPerPerson(probs_cal$control["event"], probs_cal$control["stay"], m)) - sum_e <- sum(expectedEventsPerPerson(probs_cal$experimental["event"], probs_cal$experimental["stay"], m)) - n_c <- ceiling(planned_final_events / (sum_c + ratio * sum_e)) - enroll_control_per_look <- rep(as.integer(n_c), k) - } else { - enroll_control_per_look <- normalizeEnrollmentVector(enroll_control_per_look, k, "enroll_control_per_look") - } - - if (is.null(enroll_experimental_per_look)) { - enroll_experimental_per_look <- as.integer(ceiling(enroll_control_per_look * ratio)) + missing_control_enrollment <- is.null(enroll_control_per_look) + missing_experimental_enrollment <- is.null(enroll_experimental_per_look) + if (missing_control_enrollment && missing_experimental_enrollment) { + planned_enrollment <- plannedSeasonalEnrollment(gsD_int, k, ratio) + if (is.null(planned_enrollment)) { + planned_enrollment <- calibratedSeasonalEnrollment( + ve = ve, + control_event_rate = control_event_rate, + season_length = season_length, + dropout_rate = dropout_rate, + planned_final_events = planned_final_events, + ratio = ratio, + k = k + ) + } + enroll_control_per_look <- planned_enrollment$control + enroll_experimental_per_look <- planned_enrollment$experimental } else { - enroll_experimental_per_look <- normalizeEnrollmentVector(enroll_experimental_per_look, k, "enroll_experimental_per_look") + if (!missing_control_enrollment) { + enroll_control_per_look <- normalizeEnrollmentVector(enroll_control_per_look, k, "enroll_control_per_look") + } + if (!missing_experimental_enrollment) { + enroll_experimental_per_look <- normalizeEnrollmentVector(enroll_experimental_per_look, k, "enroll_experimental_per_look") + } + if (missing_control_enrollment) { + enroll_control_per_look <- as.integer(ceiling(enroll_experimental_per_look / ratio)) + } + if (missing_experimental_enrollment) { + enroll_experimental_per_look <- as.integer(ceiling(enroll_control_per_look * ratio)) + } } all_summary <- list() @@ -263,6 +275,7 @@ simBinomialSeasonalExact <- function( adapt_looks = adapt_looks, max_multiplier = max_multiplier, usTime = usTime_resolved, + lsTime = lsTime_resolved, final_full_spending = final_full_spending ), simplify = FALSE @@ -346,6 +359,7 @@ simulateSeasonalExactTrial <- function( adapt_looks, max_multiplier, usTime, + lsTime, final_full_spending) { k <- length(planned_counts) probs <- seasonalBinomialProb(ve = ve, lambda_c = lambda_c, eta = eta_rate, season_length = season_length) @@ -356,6 +370,7 @@ simulateSeasonalExactTrial <- function( at_risk_e <- 0L cum_total <- integer(k) cum_exp <- integer(k) + cum_enrolled <- integer(k) for (season in seq_len(k)) { if (adaptive && season > 1 && (season - 1) %in% adapt_looks) { @@ -366,6 +381,7 @@ simulateSeasonalExactTrial <- function( at_risk_c <- at_risk_c + enroll_c[season] at_risk_e <- at_risk_e + enroll_e[season] + cum_enrolled[season] <- sum(enroll_c[seq_len(season)] + enroll_e[seq_len(season)]) out_c <- as.integer(stats::rmultinom(1, at_risk_c, probs$control)) out_e <- as.integer(stats::rmultinom(1, at_risk_e, probs$experimental)) @@ -386,54 +402,81 @@ simulateSeasonalExactTrial <- function( informative <- informative[c(TRUE, diff(cum_total[informative]) > 0)] } if (length(informative) == 0) { - return(list(reject = FALSE, looks = 0L, total_events = cum_total[k], total_enrolled = sum(enroll_c + enroll_e))) + last_look <- looks[length(looks)] + return(list( + reject = FALSE, + futility_stop = FALSE, + looks = 0L, + total_events = as.integer(cum_total[last_look]), + total_enrolled = as.numeric(cum_enrolled[last_look]) + )) } look_spending <- usTime[informative] + look_lower_spending <- lsTime[informative] if (isTRUE(final_full_spending)) { look_spending[length(look_spending)] <- 1 + look_lower_spending[length(look_lower_spending)] <- 1 } - bounds <- binomialExactLowerBound( - gsD, - cum_total[informative], - gsD$alpha, - fullSpendFinal = FALSE, - spendingTime = look_spending - ) - trial_exact <- tryCatch(do.call( - toBinomialExact, - list( - x = gsD, - observedEvents = cum_total[informative], - usTime = look_spending, - lsTime = if (gsD$test.type == 4) look_spending else NULL, - maxSpend = FALSE + + active_lower <- if (gsD$test.type == 4 && !is.null(gsD$testLower)) { + tl <- gsD$testLower + if (length(tl) == 1) tl <- rep(tl, k) + as.logical(tl[informative]) + } else { + rep(gsD$test.type == 4, length(informative)) + } + + trial_exact <- NULL + if (length(informative) >= 2) { + gsD_trial <- gsD + if (gsD$test.type == 4) { + gsD_trial$testLower <- active_lower + } + trial_exact <- tryCatch(do.call( + toBinomialExact, + list( + x = gsD_trial, + observedEvents = cum_total[informative], + usTime = look_spending, + lsTime = if (gsD$test.type == 4) look_lower_spending else NULL, + maxSpend = FALSE + ) + ), error = function(e) NULL) + } + bounds <- if (!is.null(trial_exact)) { + trial_exact$lower$bound + } else { + binomialExactLowerBound( + gsD, + cum_total[informative], + gsD$alpha, + fullSpendFinal = FALSE, + spendingTime = look_spending ) - ), error = function(e) NULL) + } - reject <- any(cum_exp[informative] <= bounds) + efficacy_cross <- which(cum_exp[informative] <= bounds) + reject <- length(efficacy_cross) > 0 futility_stop <- FALSE + futility_cross <- integer(0) if (gsD$test.type == 4 && !is.null(trial_exact)) { - active_lower <- if (!is.null(gsD$testLower)) { - tl <- gsD$testLower - if (length(tl) == 1) tl <- rep(tl, k) - as.logical(tl[informative]) - } else { - rep(TRUE, length(informative)) - } - efficacy_cross <- which(cum_exp[informative] <= bounds) futility_cross <- which(active_lower & (cum_exp[informative] >= trial_exact$upper$bound)) - first_eff <- if (length(efficacy_cross) > 0) min(efficacy_cross) else Inf - first_fut <- if (length(futility_cross) > 0) min(futility_cross) else Inf - futility_stop <- is.finite(first_fut) && (first_fut < first_eff) } + first_eff <- if (length(efficacy_cross) > 0) min(efficacy_cross) else Inf + first_fut <- if (length(futility_cross) > 0) min(futility_cross) else Inf + first_stop <- min(first_eff, first_fut) + stopped_on_bound <- is.finite(first_stop) + stop_position <- if (stopped_on_bound) first_stop else length(informative) + stop_look <- informative[stop_position] + futility_stop <- is.finite(first_fut) && (first_fut < first_eff) list( reject = reject, futility_stop = futility_stop, - looks = as.integer(length(informative)), - total_events = as.integer(cum_total[k]), - total_enrolled = as.numeric(sum(enroll_c + enroll_e)) + looks = as.integer(stop_position), + total_events = as.integer(cum_total[stop_look]), + total_enrolled = as.numeric(if (stopped_on_bound) cum_enrolled[stop_look] else cum_enrolled[looks[length(looks)]]) ) } @@ -465,6 +508,59 @@ expectedEventsPerPerson <- function(p_event, p_stay, m) { out } +plannedSeasonalEnrollment <- function(gsD, k, ratio) { + total <- NULL + if (!is.null(gsD$gamma) && !is.null(gsD$R)) { + gamma <- as.matrix(gsD$gamma) + periods <- as.numeric(gsD$R) + if (nrow(gamma) == length(periods) && length(periods) >= k && length(periods) %% k == 0) { + periods_per_look <- length(periods) / k + period_enrollment <- as.numeric(rowSums(gamma)) * periods + groups <- rep(seq_len(k), each = periods_per_look) + total <- as.numeric(tapply(period_enrollment, groups, sum)) + } + } + if (is.null(total) && !is.null(gsD$eNC) && !is.null(gsD$eNE)) { + cumulative <- rowSums(as.matrix(gsD$eNC)) + rowSums(as.matrix(gsD$eNE)) + if (length(cumulative) == k) { + total <- c(cumulative[1], diff(cumulative)) + } + } + if (is.null(total) || length(total) != k || any(!is.finite(total)) || any(total <= 0)) { + return(NULL) + } + total <- pmax(2L, as.integer(round(total))) + control <- as.integer(round(total / (1 + ratio))) + control <- pmin(pmax(control, 1L), total - 1L) + list( + control = control, + experimental = as.integer(total - control) + ) +} + +calibratedSeasonalEnrollment <- function( + ve, + control_event_rate, + season_length, + dropout_rate, + planned_final_events, + ratio, + k) { + ve_cal <- max(ve) + lambda_c_cal <- -log(1 - control_event_rate[which.max(ve)]) / season_length + eta_cal <- if (dropout_rate == 0) 0 else -log(1 - dropout_rate) / season_length + probs_cal <- seasonalBinomialProb(ve = ve_cal, lambda_c = lambda_c_cal, eta = eta_cal, season_length = season_length) + m <- rev(seq_len(k)) + sum_c <- sum(expectedEventsPerPerson(probs_cal$control["event"], probs_cal$control["stay"], m)) + sum_e <- sum(expectedEventsPerPerson(probs_cal$experimental["event"], probs_cal$experimental["stay"], m)) + n_c <- ceiling(planned_final_events / (sum_c + ratio * sum_e)) + control <- rep(as.integer(n_c), k) + list( + control = control, + experimental = as.integer(ceiling(control * ratio)) + ) +} + normalizeEnrollmentVector <- function(x, k, varname) { if (!is.numeric(x) || any(!is.finite(x)) || any(x != floor(x)) || any(x <= 0)) { stop(varname, " must be an integer scalar or length-k integer vector with values > 0", call. = FALSE) diff --git a/R/toInteger.R b/R/toInteger.R index e0205430..231511d5 100644 --- a/R/toInteger.R +++ b/R/toInteger.R @@ -12,8 +12,8 @@ #' non-negative integer. #' If \code{roundUpFinal = FALSE} and \code{ratio} is a non-negative integer, #' sample size is rounded to the nearest multiple of \code{ratio + 1}. -#' For event counts, \code{roundUpFinal = TRUE} rounds final event count up; -#' otherwise, just rounded if \code{roundUpFinal = FALSE}. +#' For survival designs, event counts are rounded down; \code{roundUpFinal} +#' applies to the rounded total sample size, not to event counts. #' See details. #' #' @return Output is an object of the same class as input \code{x}; i.e., @@ -25,7 +25,7 @@ #' \code{gsDesign} object is input since \code{gsDesign()} does not have a #' \code{ratio} in return. #' \code{ratio = 0, roundUpFinal = TRUE} will just round up the sample size -#' (also event count). +#' for non-survival designs. #' Rounding of event count targets is not impacted by \code{ratio}. #' Since \code{x <- gsSurv(ratio = M)} returns a value for \code{ratio}, #' \code{toInteger(x)} will round to a multiple of \code{M + 1} if \code{M} @@ -71,8 +71,7 @@ #' minfup = 8, # Planned minimum follow-up #' ratio = 3 # Randomization ratio (experimental:control) #' ) -#' # Convert sample size to multiple of ratio + 1 = 4, round event counts. -#' # Default is to round up both event count and sample size for final analysis +#' # Convert sample size to multiple of ratio + 1 = 4, round event counts down. #' toInteger(x) toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { if (!inherits(x, "gsDesign")) stop("must have class gsDesign as input") @@ -80,13 +79,16 @@ toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { message("toInteger: rounding done to nearest integer since ratio was not specified as postive integer .") ratio <- 0 } - counts <- round(x$n.I) # Round counts (event counts for survival; otherwise sample size) - # For time-to-event endpoint, just round final event count up if (inherits(x, "gsSurv")) { - if (abs(counts[x$k] - x$n.I[x$k]) <= .01){ - counts[x$k] <- round(x$n.I[x$k]) - } else if (roundUpFinal) counts[x$k] <- ceiling(x$n.I[x$k]) + counts <- ifelse(abs(x$n.I - round(x$n.I)) <= .01, round(x$n.I), floor(x$n.I)) + counts <- pmax(1, counts) + if (x$k > 1) { + for (i in 2:x$k) { + counts[i] <- max(counts[i], counts[i - 1] + 1) + } + } } else { + counts <- round(x$n.I) # Round sample size for non-survival designs # Check if control size is close to integer multiple of ratio + 1 if (abs(x$n.I[x$k] - round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1)) <= .01) { counts[x$k] <- round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) @@ -160,6 +162,23 @@ toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { alpha = x$alpha, beta = NULL, sided = 1, tol = x$tol ) xx$tol <- x$tol + rounded_up_events <- ceiling(x$n.I[x$k]) + if (rounded_up_events > xi$n.I[xi$k]) { + rounded_up_check <- tryCatch( + { + gsnSurv(xx, rounded_up_events) + TRUE + }, + error = function(e) e + ) + if (inherits(rounded_up_check, "error")) { + warning( + "toInteger: rounded-up event count is not achievable with the given enrollment model; ", + "event counts were rounded down.", + call. = FALSE + ) + } + } z <- gsnSurv(xx, xi$n.I[xi$k]) eDC <- NULL eDE <- NULL diff --git a/man/simBinomialSeasonalExact.Rd b/man/simBinomialSeasonalExact.Rd index a466f3db..bcd47000 100644 --- a/man/simBinomialSeasonalExact.Rd +++ b/man/simBinomialSeasonalExact.Rd @@ -49,12 +49,12 @@ cumulative events at analyses. If `NULL`, these are derived from used to derive `planned_counts` when `planned_counts = NULL`.} \item{enroll_control_per_look}{Optional control-arm enrollment by look -(scalar or length `k` integer vector). If `NULL`, this is calibrated -from `planned_counts` under the largest value of `ve`.} +(scalar or length `k` integer vector). If both enrollment vectors are +`NULL`, defaults are derived from the seasonal accrual pattern in `gsD`.} \item{enroll_experimental_per_look}{Optional experimental-arm enrollment by -look (scalar or length `k` integer vector). If `NULL`, this is set using -`gsD$ratio` from `enroll_control_per_look`.} +look (scalar or length `k` integer vector). If `NULL` and +`enroll_control_per_look` is supplied, this is set using `gsD$ratio`.} \item{adaptive}{Logical vector specifying whether to simulate fixed and/or adaptive enrollment scenarios.} @@ -67,7 +67,7 @@ when adaptation is enabled.} \item{usTime}{Optional upper spending-time override passed to [toBinomialExact()]. If `NULL`, spending time defaults to -`planned_counts / planned_final_events` (capped at 1).} +`1 / k, 2 / k, ..., 1`.} \item{lsTime}{Optional lower spending-time override for `test.type = 4`. If `NULL`, this defaults to `usTime`.} diff --git a/man/toInteger.Rd b/man/toInteger.Rd index bb32c8d6..a16a77f0 100644 --- a/man/toInteger.Rd +++ b/man/toInteger.Rd @@ -21,8 +21,8 @@ with the default \code{roundUpFinal = TRUE} if \code{ratio} is a non-negative integer. If \code{roundUpFinal = FALSE} and \code{ratio} is a non-negative integer, sample size is rounded to the nearest multiple of \code{ratio + 1}. -For event counts, \code{roundUpFinal = TRUE} rounds final event count up; -otherwise, just rounded if \code{roundUpFinal = FALSE}. +For survival designs, event counts are rounded down; \code{roundUpFinal} +applies to the rounded total sample size, not to event counts. See details.} } \value{ @@ -39,7 +39,7 @@ It is useful to explicitly provide the argument \code{ratio} when a \code{gsDesign} object is input since \code{gsDesign()} does not have a \code{ratio} in return. \code{ratio = 0, roundUpFinal = TRUE} will just round up the sample size -(also event count). +for non-survival designs. Rounding of event count targets is not impacted by \code{ratio}. Since \code{x <- gsSurv(ratio = M)} returns a value for \code{ratio}, \code{toInteger(x)} will round to a multiple of \code{M + 1} if \code{M} @@ -83,7 +83,6 @@ x <- gsSurv( minfup = 8, # Planned minimum follow-up ratio = 3 # Randomization ratio (experimental:control) ) -# Convert sample size to multiple of ratio + 1 = 4, round event counts. -# Default is to round up both event count and sample size for final analysis +# Convert sample size to multiple of ratio + 1 = 4, round event counts down. toInteger(x) } diff --git a/tests/testthat/test-developer-test-toInteger.R b/tests/testthat/test-developer-test-toInteger.R index 2a738987..0cbcdad0 100644 --- a/tests/testthat/test-developer-test-toInteger.R +++ b/tests/testthat/test-developer-test-toInteger.R @@ -50,19 +50,13 @@ test_that("Test: toInteger for multiple of 5", { test_that("Test: toInteger for survival endpoint event count works properly", { # This gives 252.1852 as sample size, 227.1393 as final event count x <- gsSurvCalendar(hr = 0.64) - # Should round event counts (up for final) and - # round up final event count final sample size only as well + # Should round event counts down and round up final sample size. y <- toInteger(x) - # Interim event counts rounded - expect_equal(min(abs(round(x$n.I[1:x$k - 1]) - y$n.I[1:x$k - 1])), 0) - # Final event count round up - expect_true( - (y$n.I - x$n.I)[x$k] >= 0, - info = "Test toInteger of gsSurv rounds up final event count" - ) + # Event counts rounded down + expect_equal(y$n.I, floor(x$n.I)) # Final sample size rounds to even expect_true(as.integer((y$eNC + y$eNE)[x$k]) %% 2 == 0) - # Final event count rounds up + # Final sample size rounds up expect_gte((y$eNC + y$eNE - x$eNC - x$eNE)[x$k], 0) }) diff --git a/tests/testthat/test-independent-test-simBinomialSeasonalExact.R b/tests/testthat/test-independent-test-simBinomialSeasonalExact.R index 99d58040..a4aa6a70 100644 --- a/tests/testthat/test-independent-test-simBinomialSeasonalExact.R +++ b/tests/testthat/test-independent-test-simBinomialSeasonalExact.R @@ -44,6 +44,13 @@ one_sided_design_for_sim <- function() { toInteger() } +planned_total_enrollment_by_season <- function(design) { + period_total <- as.numeric(rowSums(as.matrix(design$gamma))) * as.numeric(design$R) + periods_per_look <- length(period_total) / design$k + groups <- rep(seq_len(design$k), each = periods_per_look) + as.integer(round(tapply(period_total, groups, sum))) +} + test_that("simBinomialSeasonalExact validates core inputs", { design <- seasonal_design_for_sim() @@ -115,6 +122,69 @@ test_that("simBinomialSeasonalExact returns expected structure", { expect_null(x$trials) }) +test_that("simBinomialSeasonalExact defaults to the design seasonal enrollment plan", { + design <- seasonal_design_for_sim() + x <- simBinomialSeasonalExact( + gsD = design, + ve = c(`H1 (VE=80%)` = 0.8), + nsim = 5, + control_event_rate = 0.003, + adaptive = FALSE, + seed = 123 + ) + planned_total <- planned_total_enrollment_by_season(design) + + expect_equal( + x$planned$enroll_control_per_look + x$planned$enroll_experimental_per_look, + planned_total + ) + expect_lte(x$summary$mean_total_enrolled[1], sum(planned_total)) +}) + +test_that("simBinomialSeasonalExact reports stopping totals at first crossed bound", { + design <- gsSurv( + k = 2, + test.type = 4, + alpha = 0.025, + beta = 0.1, + timing = 0.5, + sfu = sfHSD, + sfupar = 1, + sfl = sfHSD, + sflpar = -2, + lambdaC = -log(1 - 0.2) / 0.5, + hr = 0.5, + hr0 = 0.9, + eta = 0, + gamma = c(10, 10), + R = c(1, 1), + T = 3, + minfup = 1, + ratio = 1 + ) |> + toInteger() + + x <- simBinomialSeasonalExact( + gsD = design, + ve = 0.01, + nsim = 5, + control_event_rate = 0.2, + season_length = 0.5, + dropout_rate = 0, + adaptive = FALSE, + seed = 2, + return_trials = TRUE + ) + + full_enrollment <- sum(x$planned$enroll_control_per_look + x$planned$enroll_experimental_per_look) + first_look_enrollment <- x$planned$enroll_control_per_look[1] + x$planned$enroll_experimental_per_look[1] + + expect_true(any(x$trials$futility_stop)) + expect_lte(max(x$trials$looks), design$k) + expect_lte(max(x$trials$total_enrolled), full_enrollment) + expect_true(any(x$trials$total_enrolled == first_look_enrollment)) +}) + test_that("simBinomialSeasonalExact is reproducible with seed", { design <- seasonal_design_for_sim() x1 <- simBinomialSeasonalExact( diff --git a/tests/testthat/test-independent-test-toBinomialExact.R b/tests/testthat/test-independent-test-toBinomialExact.R index 10b22c86..eac1f82a 100644 --- a/tests/testthat/test-independent-test-toBinomialExact.R +++ b/tests/testthat/test-independent-test-toBinomialExact.R @@ -73,10 +73,10 @@ test_that("toBinomialExact converts survival design to monotone integer bounds", expect_equal(result$k, design$k) expect_equal(result$n.I, expected_counts) - expect_equal(result$lower$bound, c(12, 23, 38)) - expect_equal(result$upper$bound, c(22, 30, 39)) - expect_equal(result$init_approx$a, c(12, 23, 38)) - expect_equal(result$init_approx$b, c(21, 29, 39)) + expect_equal(result$lower$bound, c(12, 23, 37)) + expect_equal(result$upper$bound, c(21, 29, 39)) + expect_equal(result$init_approx$a, c(12, 23, 37)) + expect_equal(result$init_approx$b, c(20, 28, 38)) expect_true(all(diff(result$lower$bound) >= 0)) expect_true(all(diff(result$upper$bound) >= 0)) diff --git a/tests/testthat/test-independent-test-toInteger.R b/tests/testthat/test-independent-test-toInteger.R index f8e8c607..1b145a1c 100644 --- a/tests/testthat/test-independent-test-toInteger.R +++ b/tests/testthat/test-independent-test-toInteger.R @@ -53,10 +53,10 @@ test_that("toInteger() handles gsSurv object integer conversion correctly", { result <- toInteger(x, ratio = 2) # Test if the final sample size is a multiple of ratio + 1 - expect_true(result$n.I[x$k] %% (2 + 1) == 0) + expect_equal(round(rowSums(result$eNC + result$eNE)[result$k]) %% (2 + 1), 0) - # Ensure final count is rounded up correctly when roundUpFinal is TRUE - expect_equal(result$n.I[x$k], ceiling(x$n.I[x$k])) + # Ensure event counts are rounded down for survival designs + expect_equal(result$n.I[x$k], floor(x$n.I[x$k])) }) test_that("toInteger() handles edge case where no rounding is needed", { @@ -194,3 +194,33 @@ test_that("toInteger() works for test.type 1 when x$lower is NULL", { expect_null(xi$lower) expect_s3_class(xi, "gsDesign") }) + +test_that("toInteger() rounds rare-event survival counts down when rounded-up events are not achievable", { + x <- gsSurv( + k = 3, + test.type = 4, + alpha = 0.025, + beta = 0.1, + timing = c(1 / 3, 2 / 3), + sfu = sfHSD, + sfupar = 1, + sfl = sfHSD, + sflpar = -2, + lambdaC = -log(1 - 0.0015) / 0.5, + hr = 0.2, + hr0 = 0.7, + eta = -log(1 - 0.1) / 0.5, + gamma = c(1, 0, 1, 0, 1, 0), + R = c(2, 10, 2, 10, 2, 10), + T = 42, + minfup = 6, + ratio = 1 + ) + + expect_warning( + xi <- toInteger(x), + "rounded-up event count is not achievable" + ) + expect_equal(xi$n.I, floor(x$n.I)) + expect_equal(round(rowSums(xi$eNC + xi$eNE)[xi$k]) %% 2, 0) +}) diff --git a/vignettes/SurvivalOverview.Rmd b/vignettes/SurvivalOverview.Rmd index 226f1fc6..69175230 100644 --- a/vignettes/SurvivalOverview.Rmd +++ b/vignettes/SurvivalOverview.Rmd @@ -347,7 +347,7 @@ nSurvival( ### Group sequential design Now we produce a group sequential design with a default asymmetric design with a futility bound based on $\beta$-spending. -We round interim event counts and round up the final event count to ensure the targeted power. +We round event counts down and round total sample size to an integer allocation. ```{r} k <- 2 # Total number of analyses diff --git a/vignettes/VaccineEfficacy.Rmd b/vignettes/VaccineEfficacy.Rmd index a60e4e81..04d5d30b 100644 --- a/vignettes/VaccineEfficacy.Rmd +++ b/vignettes/VaccineEfficacy.Rmd @@ -162,7 +162,7 @@ x <- gsSurv( ``` Now we convert this to a design with integer event counts at analyses. -This is achieved by rounding interim analysis event counts from the above design and rounding up the final analysis event count. +This is achieved by rounding event counts down and rounding total sample size to an integer allocation. This will result in a slight change in event fractions at interim analyses as well as a slight change from the targeted 90% power. We now explain the rationale behind the spending function choices. Recall that the hazard ratio (HR) is 1 minus the VE. diff --git a/vignettes/toInteger.Rmd b/vignettes/toInteger.Rmd index 1f46eb66..d987c3cf 100644 --- a/vignettes/toInteger.Rmd +++ b/vignettes/toInteger.Rmd @@ -37,7 +37,7 @@ x <- gsSurv(ratio = 1, hr = .74) y <- x |> toInteger() # Continuous event counts x$n.I -# Rounded event counts at interims, rounded up at final analysis +# Event counts rounded down y$n.I # Continuous sample size at interim and final analyses as.numeric(x$eNE + x$eNC) From 30daa5fb35aee76fe95113982d65557ffae39287 Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 09:19:30 -0400 Subject: [PATCH 02/23] Add cross-agent workflow instructions --- .gemini/styleguide.md | 17 +++++++++++ .github/copilot-instructions.md | 53 +++++++++++++++++++++++++++++++++ AGENTS.md | 53 +++++++++++++++++++++++++++++++++ CLAUDE.md | 53 +++++++++++++++++++++++++++++++++ GEMINI.md | 53 +++++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 .gemini/styleguide.md create mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md new file mode 100644 index 00000000..ca47a4c4 --- /dev/null +++ b/.gemini/styleguide.md @@ -0,0 +1,17 @@ +# gsDesign Agent Instructions + +Use these instructions when reviewing or modifying the `keaven/gsDesign` R package. + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. +- Prefer `rg` for code searches. +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. +- Include issue numbers when known, for example `(#264)`. +- Run focused `testthat::test_file()` checks for touched areas before broader tests. +- Before release-style commits, run `pkgload::load_all(".")` and `testthat::test_dir("tests/testthat")`. +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`; restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- Rebuild local pkgdown with `pkgdown::build_site()` when Rd, vignettes, NEWS, or local docs are updated. +- pkgdown writes to ignored `docs/`; generated site files normally do not appear in `git status`. +- Stage only intentional source, documentation, tests, NEWS, and version changes. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. From 04cc79292a458dbba9e3aa6c57c0eddb1e7c124d Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 09:21:17 -0400 Subject: [PATCH 03/23] Document coding agent instruction support --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 5e075cb5..222deb1f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -117,6 +117,9 @@ futility stopping probabilities with non-binding Type I interpretation notes. - Reorganized pkgdown article sections to separate general materials, exact binomial workflows, and multiple-hypothesis-testing content. +- Added repository-level coding agent instructions for Codex, Claude Code, + Gemini, and GitHub Copilot to support consistent gsDesign issue-branch, + testing, NEWS/version, pkgdown, commit, and push workflows. ## Testing From dbfa702a37d7b395b21beb68e527a3c2a1544439 Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 09:34:12 -0400 Subject: [PATCH 04/23] Ignore local agent directories in R builds --- .Rbuildignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index b78393ec..fb6a5c15 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,6 +4,9 @@ ^_pkgdown\.yml$ ^pkgdown$ ^\.github$ +^\.gemini$ +^\.cursor$ +^\.local-r-lib$ ^codecov\.yml$ ^LICENSE\.md$ ^cran-comments\.md$ From 8dd96c3ba31a6d280de0f8624105747ed4d448fc Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 10:10:05 -0400 Subject: [PATCH 05/23] Add AI skills vignette --- NEWS.md | 4 ++ _pkgdown.yml | 1 + inst/ai/AGENTS.md | 53 ++++++++++++++++ inst/ai/CLAUDE.md | 53 ++++++++++++++++ inst/ai/GEMINI.md | 53 ++++++++++++++++ inst/ai/README.md | 22 +++++++ inst/ai/copilot-instructions.md | 53 ++++++++++++++++ inst/ai/gemini-styleguide.md | 17 +++++ vignettes/gsDesignAISkills.Rmd | 108 ++++++++++++++++++++++++++++++++ 9 files changed, 364 insertions(+) create mode 100644 inst/ai/AGENTS.md create mode 100644 inst/ai/CLAUDE.md create mode 100644 inst/ai/GEMINI.md create mode 100644 inst/ai/README.md create mode 100644 inst/ai/copilot-instructions.md create mode 100644 inst/ai/gemini-styleguide.md create mode 100644 vignettes/gsDesignAISkills.Rmd diff --git a/NEWS.md b/NEWS.md index 222deb1f..5bbbc619 100644 --- a/NEWS.md +++ b/NEWS.md @@ -120,6 +120,10 @@ - Added repository-level coding agent instructions for Codex, Claude Code, Gemini, and GitHub Copilot to support consistent gsDesign issue-branch, testing, NEWS/version, pkgdown, commit, and push workflows. +- Added vignette "Using gsDesign AI skills" + (`vignette("gsDesignAISkills")`) and package-shipped AI workflow templates + under `inst/ai/` so users can copy the agent instructions into their own + projects after installing `gsDesign`. ## Testing diff --git a/_pkgdown.yml b/_pkgdown.yml index e73f42be..806dca16 100755 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -143,6 +143,7 @@ articles: contents: - GentleIntroductionToGSD - gsDesignPackageOverview + - gsDesignAISkills - SpendingFunctionOverview - nNormal - binomialTwoSample diff --git a/inst/ai/AGENTS.md b/inst/ai/AGENTS.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/inst/ai/AGENTS.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/CLAUDE.md b/inst/ai/CLAUDE.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/inst/ai/CLAUDE.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/GEMINI.md b/inst/ai/GEMINI.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/inst/ai/GEMINI.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/README.md b/inst/ai/README.md new file mode 100644 index 00000000..c4e0d783 --- /dev/null +++ b/inst/ai/README.md @@ -0,0 +1,22 @@ +# gsDesign AI Workflow Instructions + +This directory contains coding-agent instruction files for working on +`gsDesign` development tasks. + +The installed package path can be found with: + +```r +system.file("ai", package = "gsDesign") +``` + +Suggested uses: + +- Copy `AGENTS.md` to a repository root for Codex-style coding agents. +- Copy `CLAUDE.md` to a repository root for Claude Code. +- Copy `GEMINI.md` to a repository root for Gemini CLI. +- Copy `copilot-instructions.md` to `.github/copilot-instructions.md`. +- Copy `gemini-styleguide.md` to `.gemini/styleguide.md`. + +These files are not statistical methods documentation. They are workflow +prompts for coding agents so routine package-development tasks use consistent +testing, NEWS, version, pkgdown, commit, and push practices. diff --git a/inst/ai/copilot-instructions.md b/inst/ai/copilot-instructions.md new file mode 100644 index 00000000..08a1d785 --- /dev/null +++ b/inst/ai/copilot-instructions.md @@ -0,0 +1,53 @@ +# gsDesign Agent Instructions + +Use these instructions when working in the `keaven/gsDesign` R package. + +## Repository Workflow + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Prefer `rg` for code searches. +- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. + +## Version And NEWS + +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. + Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. +- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. +- Include the issue number when known, for example `(#264)`. + +## Testing + +- Run focused `testthat::test_file()` checks for touched areas before broad tests. +- Before release-style commits, run: + +```r +pkgload::load_all(".") +testthat::test_dir("tests/testthat") +``` + +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- When changing `DESCRIPTION`, verify: + +```r +pkgload::load_all(".") +as.character(utils::packageVersion("gsDesign")) +``` + +## pkgdown + +- Rebuild local pkgdown with: + +```r +pkgdown::build_site() +``` + +- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. + +## Commit And Push + +- Stage only intentional source, documentation, tests, NEWS, and version changes. +- Use concise issue-focused commit messages. +- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/gemini-styleguide.md b/inst/ai/gemini-styleguide.md new file mode 100644 index 00000000..ca47a4c4 --- /dev/null +++ b/inst/ai/gemini-styleguide.md @@ -0,0 +1,17 @@ +# gsDesign Agent Instructions + +Use these instructions when reviewing or modifying the `keaven/gsDesign` R package. + +- Check `git status --short --branch` before edits, staging, committing, or pulling. +- Preserve unrelated user changes. +- Prefer `rg` for code searches. +- For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. +- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. +- Include issue numbers when known, for example `(#264)`. +- Run focused `testthat::test_file()` checks for touched areas before broader tests. +- Before release-style commits, run `pkgload::load_all(".")` and `testthat::test_dir("tests/testthat")`. +- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`; restore unintended snapshot deletions before committing. +- Run `git diff --check` before staging or committing. +- Rebuild local pkgdown with `pkgdown::build_site()` when Rd, vignettes, NEWS, or local docs are updated. +- pkgdown writes to ignored `docs/`; generated site files normally do not appear in `git status`. +- Stage only intentional source, documentation, tests, NEWS, and version changes. diff --git a/vignettes/gsDesignAISkills.Rmd b/vignettes/gsDesignAISkills.Rmd new file mode 100644 index 00000000..28dabd66 --- /dev/null +++ b/vignettes/gsDesignAISkills.Rmd @@ -0,0 +1,108 @@ +--- +title: "Using gsDesign AI skills" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Using gsDesign AI skills} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +## Overview + +The `gsDesign` repository includes lightweight AI workflow instructions for +coding agents. These files are not statistical methods documentation. They are +promptable development guides that help coding agents use consistent package +maintenance habits: checking git status, updating `NEWS.md`, bumping development +versions, running focused and broad tests, rebuilding pkgdown, and pushing +issue branches. + +The repository includes native instruction files for common coding agents: + +- `AGENTS.md` for Codex-style agents. +- `CLAUDE.md` for Claude Code. +- `GEMINI.md` for Gemini CLI. +- `.github/copilot-instructions.md` for GitHub Copilot. +- `.gemini/styleguide.md` for Gemini Code Assist. + +The installed R package also ships copies under `inst/ai/`, available with: + +```{r, eval=FALSE} +system.file("ai", package = "gsDesign") +``` + +This makes the instructions available to users who install the package without +cloning the GitHub repository. + +## Installing instructions in a project + +After installing `gsDesign`, you can copy an instruction file into a local +project. For example, to add Codex-style instructions: + +```{r, eval=FALSE} +file.copy( + from = system.file("ai", "AGENTS.md", package = "gsDesign"), + to = "AGENTS.md", + overwrite = FALSE +) +``` + +For GitHub Copilot: + +```{r, eval=FALSE} +dir.create(".github", showWarnings = FALSE) +file.copy( + from = system.file("ai", "copilot-instructions.md", package = "gsDesign"), + to = file.path(".github", "copilot-instructions.md"), + overwrite = FALSE +) +``` + +For Gemini Code Assist: + +```{r, eval=FALSE} +dir.create(".gemini", showWarnings = FALSE) +file.copy( + from = system.file("ai", "gemini-styleguide.md", package = "gsDesign"), + to = file.path(".gemini", "styleguide.md"), + overwrite = FALSE +) +``` + +## Example prompt and response + +An AI skill works best when the user gives a specific development task and lets +the instruction file supply the package workflow details. + +Example prompt: + +> Use the gsDesign workflow instructions. Fix issue #264 on the current branch: +> update `simBinomialSeasonalExact()` so simulated trials stop at efficacy or +> futility boundary crossings, update `toInteger()` for rare-event equal +> allocation, add regression tests, update `NEWS.md`, rebuild pkgdown, run the +> package checks, commit, and push if everything is OK. + +Example response: + +> I checked the branch and worktree, implemented the simulation stopping and +> event-rounding updates, added focused regression tests, bumped the package to +> the next development version, updated `NEWS.md`, rebuilt pkgdown, and ran +> `R CMD check --no-manual`. The only remaining check warning is a local +> macOS/R toolchain warning from `R_ext/Boolean.h`, not package code. I committed +> the changes and pushed the issue branch to `origin`. + +The response is useful because it reports the workflow steps the agent took and +separates package-code results from local toolchain noise. + +## What these instructions do not do + +AI workflow instructions do not change the statistical behavior of `gsDesign`. +They also do not replace human review. They help make agent-assisted package +maintenance more reproducible by placing the expected repository workflow where +coding agents can find it. From b1eae2f0bc58fd725e95265e6b246beb446fcef2 Mon Sep 17 00:00:00 2001 From: keaven Date: Fri, 15 May 2026 11:58:35 -0400 Subject: [PATCH 06/23] Clarify AI design function selection --- .gemini/styleguide.md | 5 ++ .github/copilot-instructions.md | 10 ++++ AGENTS.md | 10 ++++ CLAUDE.md | 10 ++++ GEMINI.md | 10 ++++ NEWS.md | 6 +++ inst/ai/AGENTS.md | 10 ++++ inst/ai/CLAUDE.md | 10 ++++ inst/ai/GEMINI.md | 10 ++++ inst/ai/README.md | 10 +++- inst/ai/copilot-instructions.md | 10 ++++ inst/ai/gemini-styleguide.md | 5 ++ vignettes/gsDesignAISkills.Rmd | 87 ++++++++++++++++++++++++++------- 13 files changed, 173 insertions(+), 20 deletions(-) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index ca47a4c4..6460e4dc 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -5,6 +5,11 @@ Use these instructions when reviewing or modifying the `keaven/gsDesign` R packa - Check `git status --short --branch` before edits, staging, committing, or pulling. - Preserve unrelated user changes. - Prefer `rg` for code searches. +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`; for example, "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` for event-driven or information-fraction timing. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but use `simBinomialSeasonalExact()`/`toBinomialExact()` only when the endpoint/workflow is seasonal rare-event exact-binomial monitoring. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. - Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. - Include issue numbers when known, for example `(#264)`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 08a1d785..55ed1d7c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/AGENTS.md b/AGENTS.md index 08a1d785..55ed1d7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/CLAUDE.md b/CLAUDE.md index 08a1d785..55ed1d7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/GEMINI.md b/GEMINI.md index 08a1d785..55ed1d7c 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/NEWS.md b/NEWS.md index 5bbbc619..3b2f69e1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -124,6 +124,12 @@ (`vignette("gsDesignAISkills")`) and package-shipped AI workflow templates under `inst/ai/` so users can copy the agent instructions into their own projects after installing `gsDesign`. +- Expanded the AI instructions and vignette example with function-selection + guidance that maps calendar-timed survival-design prompts, such as a + 24-month interim after enrollment opens with `ratio = 1`, to + `gsSurvCalendar()` while preserving the original design specifications, and + clarifies that very low event counts can cue discussion of exact-binomial + rare-event methods without automatically changing the design family. ## Testing diff --git a/inst/ai/AGENTS.md b/inst/ai/AGENTS.md index 08a1d785..55ed1d7c 100644 --- a/inst/ai/AGENTS.md +++ b/inst/ai/AGENTS.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/inst/ai/CLAUDE.md b/inst/ai/CLAUDE.md index 08a1d785..55ed1d7c 100644 --- a/inst/ai/CLAUDE.md +++ b/inst/ai/CLAUDE.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/inst/ai/GEMINI.md b/inst/ai/GEMINI.md index 08a1d785..55ed1d7c 100644 --- a/inst/ai/GEMINI.md +++ b/inst/ai/GEMINI.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/inst/ai/README.md b/inst/ai/README.md index c4e0d783..0c846614 100644 --- a/inst/ai/README.md +++ b/inst/ai/README.md @@ -17,6 +17,12 @@ Suggested uses: - Copy `copilot-instructions.md` to `.github/copilot-instructions.md`. - Copy `gemini-styleguide.md` to `.gemini/styleguide.md`. -These files are not statistical methods documentation. They are workflow +These files are not full statistical methods documentation. They are workflow prompts for coding agents so routine package-development tasks use consistent -testing, NEWS, version, pkgdown, commit, and push practices. +testing, NEWS, version, pkgdown, commit, and push practices. They also include +brief function-selection guidance, such as using `gsSurvCalendar()` when a +request specifies analyses at calendar months after enrollment opens, while +preserving the original design specifications unless the user asks to change +them. Very low planned event counts can cue a discussion of exact-binomial +rare-event methods, but should not by itself override a calendar-timed survival +design request. diff --git a/inst/ai/copilot-instructions.md b/inst/ai/copilot-instructions.md index 08a1d785..55ed1d7c 100644 --- a/inst/ai/copilot-instructions.md +++ b/inst/ai/copilot-instructions.md @@ -9,6 +9,16 @@ Use these instructions when working in the `keaven/gsDesign` R package. - Prefer `rg` for code searches. - When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +## gsDesign Function Selection + +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. + ## Version And NEWS - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. diff --git a/inst/ai/gemini-styleguide.md b/inst/ai/gemini-styleguide.md index ca47a4c4..6460e4dc 100644 --- a/inst/ai/gemini-styleguide.md +++ b/inst/ai/gemini-styleguide.md @@ -5,6 +5,11 @@ Use these instructions when reviewing or modifying the `keaven/gsDesign` R packa - Check `git status --short --branch` before edits, staging, committing, or pulling. - Preserve unrelated user changes. - Prefer `rg` for code searches. +- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`; for example, "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. +- Use `gsSurv()` for event-driven or information-fraction timing. +- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but use `simBinomialSeasonalExact()`/`toBinomialExact()` only when the endpoint/workflow is seasonal rare-event exact-binomial monitoring. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. - Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. - Include issue numbers when known, for example `(#264)`. diff --git a/vignettes/gsDesignAISkills.Rmd b/vignettes/gsDesignAISkills.Rmd index 28dabd66..8d571882 100644 --- a/vignettes/gsDesignAISkills.Rmd +++ b/vignettes/gsDesignAISkills.Rmd @@ -17,11 +17,16 @@ knitr::opts_chunk$set( ## Overview The `gsDesign` repository includes lightweight AI workflow instructions for -coding agents. These files are not statistical methods documentation. They are -promptable development guides that help coding agents use consistent package +coding agents. These files are not full statistical methods documentation. They +are promptable development guides that help coding agents use consistent package maintenance habits: checking git status, updating `NEWS.md`, bumping development versions, running focused and broad tests, rebuilding pkgdown, and pushing -issue branches. +issue branches. They also include brief function-selection cues for common +design requests, such as using `gsSurvCalendar()` when the prompt fixes analyses +at calendar months after enrollment opens, while preserving the original design +specifications unless the user asks to change them. Very low planned event +counts can cue a discussion of exact-binomial rare-event methods, but should not +by itself override a calendar-timed survival design request. The repository includes native instruction files for common coding agents: @@ -77,28 +82,74 @@ file.copy( ## Example prompt and response -An AI skill works best when the user gives a specific development task and lets -the instruction file supply the package workflow details. +An AI skill works best when the user gives a specific package-development or +design-coding task and lets the instruction file supply the package workflow +and routing details. Example prompt: -> Use the gsDesign workflow instructions. Fix issue #264 on the current branch: -> update `simBinomialSeasonalExact()` so simulated trials stop at efficacy or -> futility boundary crossings, update `toInteger()` for rare-event equal -> allocation, add regression tests, update `NEWS.md`, rebuild pkgdown, run the -> package checks, commit, and push if everything is OK. +> Use the gsDesign AI instructions. I am designing a standard time-to-event +> group sequential trial, not a seasonal rare-event exact-binomial example. +> Start with the original design specs: non-binding futility (`test.type = 4`), +> information-based spending, Hwang-Shih-DeCani efficacy spending with +> `sfupar = -4`, Hwang-Shih-DeCani futility spending with `sflpar = -2`, +> one-sided alpha 0.025, beta 0.1, a control median of 12 months, null HR 1, +> alternative HR 0.7, no dropout, uniform enrollment over 18 months, and +> minimum follow-up of 18 months. Keep those specs. Use equal randomization and +> add another interim analysis at 24 months after enrollment opens, so analyses +> occur at 12, 24, and 36 months. Show the R code and explain which gsDesign +> function matches this text. Example response: -> I checked the branch and worktree, implemented the simulation stopping and -> event-rounding updates, added focused regression tests, bumped the package to -> the next development version, updated `NEWS.md`, rebuilt pkgdown, and ran -> `R CMD check --no-manual`. The only remaining check warning is a local -> macOS/R toolchain warning from `R_ext/Boolean.h`, not package code. I committed -> the changes and pushed the issue branch to `origin`. +> The phrase "analyses at 12, 24, and 36 months after enrollment opens" fixes +> the analysis schedule on the calendar scale, so I would use +> `gsSurvCalendar(calendarTime = c(12, 24, 36))`. The timing request changes +> the analysis schedule only; the original test type, spending functions, +> alpha, beta, hazard assumptions, dropout, accrual, and follow-up inputs are +> preserved. I would not use `simBinomialSeasonalExact()` because the prompt +> does not describe a seasonal rare-event exact-binomial monitoring problem. +> If the resulting planned event counts are very low, such as fewer than 100 +> events, I might mention exact-binomial rare-event methods as a sensitivity or +> alternative workflow to consider, but I would not switch to them unless the +> endpoint and monitoring plan are actually binomial rare-event based. Equal +> randomization is `ratio = 1`. + +```r +library(gsDesign) + +design <- gsSurvCalendar( + calendarTime = c(12, 24, 36), + spending = "information", + test.type = 4, + alpha = 0.025, + beta = 0.10, + sfu = sfHSD, + sfupar = -4, + sfl = sfHSD, + sflpar = -2, + lambdaC = log(2) / 12, + hr = 0.70, + hr0 = 1, + eta = 0, + gamma = 1, + R = 18, + minfup = 18, + ratio = 1 +) + +design_integer <- toInteger(design) +gsBoundSummary(design_integer) +``` -The response is useful because it reports the workflow steps the agent took and -separates package-code results from local toolchain noise. +The response is useful because it maps the user's timing language to the +appropriate `gsDesign` function before writing code. It also distinguishes +calendar timing from calendar spending: `spending = "information"` keeps +spending tied to information, while `spending = "calendar"` would tie spending +to the calendar fractions. The original design specifications are kept in the +function call rather than being replaced by generic defaults. The low-count +heuristic is treated as a prompt to discuss alternatives, not as permission to +change the design family. ## What these instructions do not do From f577a156f0b97b1a2072b2f2317ecc63372407d8 Mon Sep 17 00:00:00 2001 From: keaven Date: Mon, 25 May 2026 14:34:09 -0400 Subject: [PATCH 07/23] Prepare gsDesign 3.10.0 release --- .Rbuildignore | 6 + DESCRIPTION | 2 +- NEWS.md | 21 ++- R/toInteger.R | 138 ++++++++++---- man/toInteger.Rd | 52 +++++- .../testthat/test-developer-test-toInteger.R | 6 +- .../test-independent-test-toBinomialExact.R | 8 +- .../test-independent-test-toInteger.R | 66 ++++++- vignettes/MultiSeasonRareEvents.Rmd | 168 ++++++++++------- vignettes/VaccineEfficacy.Rmd | 1 + vignettes/toInteger.Rmd | 173 +++++++++++++++--- 11 files changed, 488 insertions(+), 153 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index fb6a5c15..bc952557 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -7,6 +7,12 @@ ^\.gemini$ ^\.cursor$ ^\.local-r-lib$ +^AGENTS\.md$ +^CLAUDE\.md$ +^GEMINI\.md$ +^doc$ +^docs$ +^Meta$ ^codecov\.yml$ ^LICENSE\.md$ ^cran-comments\.md$ diff --git a/DESCRIPTION b/DESCRIPTION index 09e9b1c7..3508e7fd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: gsDesign -Version: 3.9.1.9000 +Version: 3.10.0 Title: Group Sequential Design Authors@R: c( person("Keaven", "Anderson", email = "keaven_anderson@merck.com", role = c("aut", "cre")), diff --git a/NEWS.md b/NEWS.md index 3b2f69e1..1b7397f9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# gsDesign (development version) +# gsDesign 3.10.0 (May 2026) ## New features @@ -77,10 +77,13 @@ `test.type` 7 or 8) when recomputing the design after integer sample size or event-count rounding. Previously the internal `gsDesign()` call omitted these settings, so inactive looks could incorrectly become active. -- `toInteger()` now rounds survival-design event counts down while still - rounding enrollment to the requested allocation multiple. A warning is issued - when a rounded-up event count is not achievable under the given enrollment - model, avoiding rare-event failures with equal allocation (#264). +- `toInteger()` now preserves the intended survival-design behavior that + `roundUpFinal = TRUE` rounds the final event count up. If the independently + rounded final sample size, using the usual `ratio + 1` allocation multiple, + cannot support the integer event target, `toInteger()` adjusts sample size by + allocation multiples, with a warning, until the target is achievable. Designs + where the rounded sample size already supports the integer event target retain + the previous behavior (#264). - Fixed sign inconsistency in `hrn2z()` which used `sign(hr0 - hr1)` while `zn2hr()` used `sign(hr1 - hr0)`, preventing correct round-trip conversion. Both now use `sign(hr1 - hr0)` (@keaven, #251). @@ -92,6 +95,14 @@ ## Documentation +- Corrected and generalized the multi-season rare-event vignette so enrollment + timing, planned counts, and simulation event-rate inputs are derived from the + stated design specifications, with calendar-timed seasonal analyses, + piecewise seasonal failure hazards, and cross-references to the exact + binomial vaccine-efficacy vignette (#264). +- Expanded `toInteger()` help and vignette guidance for survival-design final + event rounding, final sample-size feasibility adjustment, and seasonal designs + with a final zero event-rate period (#264). - Documented `test.type` restriction in `toBinomialExact()`: only `test.type = 1` and `4` are supported; other types (including 7 and 8) produce an error (@keaven, #109). diff --git a/R/toInteger.R b/R/toInteger.R index 231511d5..5dde1b8e 100644 --- a/R/toInteger.R +++ b/R/toInteger.R @@ -7,13 +7,13 @@ #' \code{ratio + 1}. See details. #' If input is non integer, rounding is done to the nearest integer or #' nearest larger integer depending on \code{roundUpFinal}. -#' @param roundUpFinal Sample size is rounded up to a value of \code{ratio + 1} -#' with the default \code{roundUpFinal = TRUE} if \code{ratio} is a -#' non-negative integer. +#' @param roundUpFinal For non-survival designs, final sample size is rounded +#' up to a multiple of \code{ratio + 1} with the default +#' \code{roundUpFinal = TRUE} if \code{ratio} is a non-negative integer. +#' For survival designs, the final event count is rounded up with +#' \code{roundUpFinal = TRUE}. #' If \code{roundUpFinal = FALSE} and \code{ratio} is a non-negative integer, #' sample size is rounded to the nearest multiple of \code{ratio + 1}. -#' For survival designs, event counts are rounded down; \code{roundUpFinal} -#' applies to the rounded total sample size, not to event counts. #' See details. #' #' @return Output is an object of the same class as input \code{x}; i.e., @@ -26,13 +26,12 @@ #' \code{ratio} in return. #' \code{ratio = 0, roundUpFinal = TRUE} will just round up the sample size #' for non-survival designs. -#' Rounding of event count targets is not impacted by \code{ratio}. #' Since \code{x <- gsSurv(ratio = M)} returns a value for \code{ratio}, #' \code{toInteger(x)} will round to a multiple of \code{M + 1} if \code{M} #' is a non-negative integer; otherwise, just rounding will occur. -#' The most common example would be if there is 1:1 randomization (2:1) and -#' the user wishes an even (multiple of 3) sample size, then \code{toInteger()} -#' will operate as expected. +#' For 1:1 randomization, \code{ratio = 1} gives an even final sample size. +#' For 2:1 randomization, \code{ratio = 2} gives a final sample size that is +#' a multiple of 3. #' To just round without concern for randomization ratio, set \code{ratio = 0}. #' If \code{toInteger(x, ratio = 3)}, rounding for final sample size is done #' to a multiple of 3 + 1 = 4; this could represent a 3:1 or 1:3 @@ -40,11 +39,42 @@ #' For 3:2 randomization, \code{ratio = 4} would ensure rounding sample size #' to a multiple of 5. #' +#' For a \code{gsSurv} object, \code{x$n.I} is an event-count schedule. +#' \code{toInteger()} first converts each planned event count to an integer. +#' Interim event counts are rounded to the nearest integer. The final event +#' count is rounded up when \code{roundUpFinal = TRUE}; otherwise, it is rounded +#' to the nearest integer. Values within 0.01 of an integer are rounded to that +#' integer. Counts are constrained to be positive and strictly increasing. The +#' group sequential boundaries and spending are then recomputed with +#' \code{gsDesign()} at the integer event counts. +#' +#' Total sample size for a survival design is handled separately. The final +#' expected total enrollment is rounded to a multiple of \code{ratio + 1}, +#' rounded up when \code{roundUpFinal = TRUE} and rounded to the nearest such +#' multiple otherwise. Enrollment rates are scaled to achieve that rounded total +#' over the original calendar plan, and final and interim analysis times are +#' recalculated to match the integer event targets. +#' +#' In seasonal or otherwise piecewise survival designs, the independently +#' rounded final sample size from this usual rule can make the final integer event target +#' unattainable. If the rounded sample size is too small to ever reach the event +#' target, \code{toInteger()} increases the sample size by allocation multiples. +#' If the rounded sample size already implies more expected events than a lower +#' event target at the earliest feasible final analysis, \code{toInteger()} +#' reduces the sample size by allocation multiples. Either adjustment issues a +#' warning. Designs where the initially rounded sample size already supports the +#' integer event target retain the previous behavior. +#' For a complete seasonal exact-binomial monitoring workflow, see +#' \code{vignette("MultiSeasonRareEvents", package = "gsDesign")}. +#' #' Selective-bound settings (\code{testUpper}, \code{testLower}, \code{testHarm}, #' and harm spending for \code{test.type} 7 or 8) are carried from the input #' design into the internal \code{gsDesign()} recomputation so skipped looks stay #' skipped after integer rounding. #' +#' @seealso \code{\link{gsSurv}}, \code{\link{toBinomialExact}}, +#' \code{vignette("MultiSeasonRareEvents", package = "gsDesign")} +#' #' @export #' #' @examples @@ -71,7 +101,8 @@ #' minfup = 8, # Planned minimum follow-up #' ratio = 3 # Randomization ratio (experimental:control) #' ) -#' # Convert sample size to multiple of ratio + 1 = 4, round event counts down. +#' # Convert sample size to multiple of ratio + 1 = 4, +#' # with final event count rounded up by default. #' toInteger(x) toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { if (!inherits(x, "gsDesign")) stop("must have class gsDesign as input") @@ -80,7 +111,12 @@ toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { ratio <- 0 } if (inherits(x, "gsSurv")) { - counts <- ifelse(abs(x$n.I - round(x$n.I)) <= .01, round(x$n.I), floor(x$n.I)) + counts <- round(x$n.I) + if (abs(x$n.I[x$k] - round(x$n.I[x$k])) <= .01) { + counts[x$k] <- round(x$n.I[x$k]) + } else if (roundUpFinal) { + counts[x$k] <- ceiling(x$n.I[x$k]) + } counts <- pmax(1, counts) if (x$k > 1) { for (i in 2:x$k) { @@ -94,7 +130,7 @@ toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { counts[x$k] <- round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) # For non-survival designs round sample size based on randomization ratio }else if (roundUpFinal) { - counts[x$k] <- ceiling(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) # Round up for final count + counts[x$k] <- ceiling(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) # Round up for final sample size } else { counts[x$k] <- round(x$n.I[x$k] / (ratio + 1)) * (ratio + 1) } @@ -153,33 +189,63 @@ toInteger <- function(x, ratio = x$ratio, roundUpFinal = TRUE) { } else { N <- round(N, 0) * (ratio + 1) } - # Update enrollment rates to achieve new sample size in same time - inflateN <- N / N_continuous - # Following is adapted from gsSurv() to construct gsSurv object - xx <- nSurv( - lambdaC = x$lambdaC, hr = x$hr, hr0 = x$hr0, eta = x$etaC, etaE = x$etaE, - gamma = x$gamma * inflateN, R = x$R, S = x$S, T = max(x$T), minfup = x$minfup, ratio = x$ratio, - alpha = x$alpha, beta = NULL, sided = 1, tol = x$tol - ) - xx$tol <- x$tol - rounded_up_events <- ceiling(x$n.I[x$k]) - if (rounded_up_events > xi$n.I[xi$k]) { - rounded_up_check <- tryCatch( - { - gsnSurv(xx, rounded_up_events) - TRUE - }, - error = function(e) e + build_nsurv <- function(N_target) { + # Update enrollment rates to achieve new sample size in same time + inflateN <- N_target / N_continuous + # Following is adapted from gsSurv() to construct gsSurv object + xx <- nSurv( + lambdaC = x$lambdaC, hr = x$hr, hr0 = x$hr0, eta = x$etaC, etaE = x$etaE, + gamma = x$gamma * inflateN, R = x$R, S = x$S, T = max(x$T), minfup = x$minfup, ratio = x$ratio, + alpha = x$alpha, beta = NULL, sided = 1, tol = x$tol ) - if (inherits(rounded_up_check, "error")) { - warning( - "toInteger: rounded-up event count is not achievable with the given enrollment model; ", - "event counts were rounded down.", - call. = FALSE - ) + xx$tol <- x$tol + xx + } + xx <- build_nsurv(N) + z <- tryCatch(gsnSurv(xx, xi$n.I[xi$k]), error = function(e) e) + if (inherits(z, "error")) { + z_error <- z + z_message <- conditionMessage(z) + N_original <- N + N_step <- ratio + 1 + if (grepl("under-powered for any follow-up duration", z_message, fixed = TRUE)) { + N_candidate <- N + N_step + adjustment <- "increased" + next_N <- function(N_current) N_current + N_step + keep_going <- function(N_current) is.finite(N_current) + } else if (grepl("over-powered for any follow-up duration", z_message, fixed = TRUE)) { + N_candidate <- N - N_step + adjustment <- "reduced" + next_N <- function(N_current) N_current - N_step + keep_going <- function(N_current) N_current >= N_step + } else { + stop(conditionMessage(z), call. = FALSE) + } + n_adjustments <- 0 + while (keep_going(N_candidate) && inherits(z, "error") && n_adjustments < 10000) { + xx_candidate <- build_nsurv(N_candidate) + z_candidate <- tryCatch(gsnSurv(xx_candidate, xi$n.I[xi$k]), error = function(e) e) + if (!inherits(z_candidate, "error")) { + N <- N_candidate + xx <- xx_candidate + z <- z_candidate + } else { + N_candidate <- next_N(N_candidate) + n_adjustments <- n_adjustments + 1 + } + } + if (inherits(z, "error")) { + stop(conditionMessage(z_error), call. = FALSE) } + warning( + "toInteger: rounded total sample size was ", adjustment, " from ", N_original, + " to ", N, " to make the integer event target achievable with the enrollment model.", + call. = FALSE + ) + } + if (inherits(z, "error")) { + stop(conditionMessage(z), call. = FALSE) } - z <- gsnSurv(xx, xi$n.I[xi$k]) eDC <- NULL eDE <- NULL eDC0 <- NULL diff --git a/man/toInteger.Rd b/man/toInteger.Rd index a16a77f0..cb39ed5d 100644 --- a/man/toInteger.Rd +++ b/man/toInteger.Rd @@ -16,13 +16,13 @@ If an integer is provided, rounding is done to a multiple of If input is non integer, rounding is done to the nearest integer or nearest larger integer depending on \code{roundUpFinal}.} -\item{roundUpFinal}{Sample size is rounded up to a value of \code{ratio + 1} -with the default \code{roundUpFinal = TRUE} if \code{ratio} is a -non-negative integer. +\item{roundUpFinal}{For non-survival designs, final sample size is rounded +up to a multiple of \code{ratio + 1} with the default +\code{roundUpFinal = TRUE} if \code{ratio} is a non-negative integer. +For survival designs, the final event count is rounded up with +\code{roundUpFinal = TRUE}. If \code{roundUpFinal = FALSE} and \code{ratio} is a non-negative integer, sample size is rounded to the nearest multiple of \code{ratio + 1}. -For survival designs, event counts are rounded down; \code{roundUpFinal} -applies to the rounded total sample size, not to event counts. See details.} } \value{ @@ -40,13 +40,12 @@ It is useful to explicitly provide the argument \code{ratio} when a \code{ratio} in return. \code{ratio = 0, roundUpFinal = TRUE} will just round up the sample size for non-survival designs. -Rounding of event count targets is not impacted by \code{ratio}. Since \code{x <- gsSurv(ratio = M)} returns a value for \code{ratio}, \code{toInteger(x)} will round to a multiple of \code{M + 1} if \code{M} is a non-negative integer; otherwise, just rounding will occur. -The most common example would be if there is 1:1 randomization (2:1) and -the user wishes an even (multiple of 3) sample size, then \code{toInteger()} -will operate as expected. +For 1:1 randomization, \code{ratio = 1} gives an even final sample size. +For 2:1 randomization, \code{ratio = 2} gives a final sample size that is +a multiple of 3. To just round without concern for randomization ratio, set \code{ratio = 0}. If \code{toInteger(x, ratio = 3)}, rounding for final sample size is done to a multiple of 3 + 1 = 4; this could represent a 3:1 or 1:3 @@ -54,11 +53,43 @@ randomization ratio. For 3:2 randomization, \code{ratio = 4} would ensure rounding sample size to a multiple of 5. +For a \code{gsSurv} object, \code{x$n.I} is an event-count schedule. +\code{toInteger()} first converts each planned event count to an integer. +Interim event counts are rounded to the nearest integer. The final event +count is rounded up when \code{roundUpFinal = TRUE}; otherwise, it is rounded +to the nearest integer. Values within 0.01 of an integer are rounded to that +integer. Counts are constrained to be positive and strictly increasing. The +group sequential boundaries and spending are then recomputed with +\code{gsDesign()} at the integer event counts. + +Total sample size for a survival design is handled separately. The final +expected total enrollment is rounded to a multiple of \code{ratio + 1}, +rounded up when \code{roundUpFinal = TRUE} and rounded to the nearest such +multiple otherwise. Enrollment rates are scaled to achieve that rounded total +over the original calendar plan, and final and interim analysis times are +recalculated to match the integer event targets. + +In seasonal or otherwise piecewise survival designs, the independently +rounded final sample size from this usual rule can make the final integer event target +unattainable. If the rounded sample size is too small to ever reach the event +target, \code{toInteger()} increases the sample size by allocation multiples. +If the rounded sample size already implies more expected events than a lower +event target at the earliest feasible final analysis, \code{toInteger()} +reduces the sample size by allocation multiples. Either adjustment issues a +warning. Designs where the initially rounded sample size already supports the +integer event target retain the previous behavior. +For a complete seasonal exact-binomial monitoring workflow, see +\code{vignette("MultiSeasonRareEvents", package = "gsDesign")}. + Selective-bound settings (\code{testUpper}, \code{testLower}, \code{testHarm}, and harm spending for \code{test.type} 7 or 8) are carried from the input design into the internal \code{gsDesign()} recomputation so skipped looks stay skipped after integer rounding. } +\seealso{ +\code{\link{gsSurv}}, \code{\link{toBinomialExact}}, + \code{vignette("MultiSeasonRareEvents", package = "gsDesign")} +} \examples{ # The following code derives the group sequential design using the method # of Lachin and Foulkes @@ -83,6 +114,7 @@ x <- gsSurv( minfup = 8, # Planned minimum follow-up ratio = 3 # Randomization ratio (experimental:control) ) -# Convert sample size to multiple of ratio + 1 = 4, round event counts down. +# Convert sample size to multiple of ratio + 1 = 4, +# with final event count rounded up by default. toInteger(x) } diff --git a/tests/testthat/test-developer-test-toInteger.R b/tests/testthat/test-developer-test-toInteger.R index 0cbcdad0..671db3f1 100644 --- a/tests/testthat/test-developer-test-toInteger.R +++ b/tests/testthat/test-developer-test-toInteger.R @@ -50,11 +50,11 @@ test_that("Test: toInteger for multiple of 5", { test_that("Test: toInteger for survival endpoint event count works properly", { # This gives 252.1852 as sample size, 227.1393 as final event count x <- gsSurvCalendar(hr = 0.64) - # Should round event counts down and round up final sample size. + # Should round interim event counts and round up final event count and sample size. y <- toInteger(x) - # Event counts rounded down - expect_equal(y$n.I, floor(x$n.I)) + # Event counts converted to integers + expect_equal(y$n.I, c(round(x$n.I[1:(x$k - 1)]), ceiling(x$n.I[x$k]))) # Final sample size rounds to even expect_true(as.integer((y$eNC + y$eNE)[x$k]) %% 2 == 0) # Final sample size rounds up diff --git a/tests/testthat/test-independent-test-toBinomialExact.R b/tests/testthat/test-independent-test-toBinomialExact.R index eac1f82a..10b22c86 100644 --- a/tests/testthat/test-independent-test-toBinomialExact.R +++ b/tests/testthat/test-independent-test-toBinomialExact.R @@ -73,10 +73,10 @@ test_that("toBinomialExact converts survival design to monotone integer bounds", expect_equal(result$k, design$k) expect_equal(result$n.I, expected_counts) - expect_equal(result$lower$bound, c(12, 23, 37)) - expect_equal(result$upper$bound, c(21, 29, 39)) - expect_equal(result$init_approx$a, c(12, 23, 37)) - expect_equal(result$init_approx$b, c(20, 28, 38)) + expect_equal(result$lower$bound, c(12, 23, 38)) + expect_equal(result$upper$bound, c(22, 30, 39)) + expect_equal(result$init_approx$a, c(12, 23, 38)) + expect_equal(result$init_approx$b, c(21, 29, 39)) expect_true(all(diff(result$lower$bound) >= 0)) expect_true(all(diff(result$upper$bound) >= 0)) diff --git a/tests/testthat/test-independent-test-toInteger.R b/tests/testthat/test-independent-test-toInteger.R index 1b145a1c..bf7d8974 100644 --- a/tests/testthat/test-independent-test-toInteger.R +++ b/tests/testthat/test-independent-test-toInteger.R @@ -54,9 +54,22 @@ test_that("toInteger() handles gsSurv object integer conversion correctly", { # Test if the final sample size is a multiple of ratio + 1 expect_equal(round(rowSums(result$eNC + result$eNE)[result$k]) %% (2 + 1), 0) + expect_equal( + rowSums(result$eNC + result$eNE)[result$k], + ceiling(rowSums(x$eNC + x$eNE)[x$k] / 3) * 3, + tolerance = 1e-5 + ) + + # Ensure final event count is rounded up for survival designs + expect_equal(result$n.I[x$k], ceiling(x$n.I[x$k])) - # Ensure event counts are rounded down for survival designs - expect_equal(result$n.I[x$k], floor(x$n.I[x$k])) + result_nearest <- toInteger(x, ratio = 2, roundUpFinal = FALSE) + expect_equal(result_nearest$n.I[x$k], round(x$n.I[x$k])) + expect_equal( + rowSums(result_nearest$eNC + result_nearest$eNE)[result_nearest$k], + round(rowSums(x$eNC + x$eNE)[x$k] / 3) * 3, + tolerance = 1e-5 + ) }) test_that("toInteger() handles edge case where no rounding is needed", { @@ -195,7 +208,7 @@ test_that("toInteger() works for test.type 1 when x$lower is NULL", { expect_s3_class(xi, "gsDesign") }) -test_that("toInteger() rounds rare-event survival counts down when rounded-up events are not achievable", { +test_that("toInteger() increases enrollment when rounded-up events are not achievable", { x <- gsSurv( k = 3, test.type = 4, @@ -219,8 +232,51 @@ test_that("toInteger() rounds rare-event survival counts down when rounded-up ev expect_warning( xi <- toInteger(x), - "rounded-up event count is not achievable" + "rounded total sample size was increased" ) - expect_equal(xi$n.I, floor(x$n.I)) + expect_equal(xi$n.I, c(round(x$n.I[1:2]), ceiling(x$n.I[x$k]))) expect_equal(round(rowSums(xi$eNC + xi$eNE)[xi$k]) %% 2, 0) + expect_equal(rowSums(xi$eDC + xi$eDE)[xi$k], xi$n.I[xi$k], tolerance = 1e-5) +}) + +test_that("toInteger() handles seasonal survival designs with final zero event rate", { + x <- gsSurv( + k = 3, + test.type = 4, + alpha = 0.025, + beta = 0.1, + timing = c(1 / 3, 2 / 3), + sfu = sfHSD, + sfupar = 1, + sfl = sfHSD, + sflpar = -2, + lambdaC = c( + -log(1 - 0.003) / 0.5, 0, + -log(1 - 0.003) / 0.5, 0, + -log(1 - 0.003) / 0.5, 0 + ), + S = c(6, 6, 6, 6, 6), + hr = 0.2, + hr0 = 0.7, + eta = -log(1 - 0.1) / 0.5, + gamma = c(1, 0, 1, 0, 1, 0), + R = c(2, 10, 2, 10, 2, 10), + T = 42, + minfup = 6, + ratio = 3, + testLower = c(TRUE, FALSE, FALSE) + ) + + expect_warning( + xi <- toInteger(x), + NA + ) + expect_equal(xi$n.I, c(round(x$n.I[1:2]), ceiling(x$n.I[x$k]))) + expect_equal(round(rowSums(xi$eNC + xi$eNE)[xi$k]) %% 4, 0) + expect_equal( + rowSums(xi$eNC + xi$eNE)[xi$k], + ceiling(rowSums(x$eNC + x$eNE)[x$k] / 4) * 4, + tolerance = 1e-5 + ) + expect_equal(rowSums(xi$eDC + xi$eDE)[xi$k], xi$n.I[xi$k], tolerance = 1e-5) }) diff --git a/vignettes/MultiSeasonRareEvents.Rmd b/vignettes/MultiSeasonRareEvents.Rmd index 0daef1fc..66bb1ba9 100644 --- a/vignettes/MultiSeasonRareEvents.Rmd +++ b/vignettes/MultiSeasonRareEvents.Rmd @@ -35,10 +35,12 @@ This vignette demonstrates a practical design workflow for rare seasonal events. The motivating use case is a vaccine efficacy trial with annual high-risk seasons. The same ideas can be adapted to infectious disease studies where simple superiority or non-inferiority may be considered. +For a general introduction to exact-binomial vaccine efficacy monitoring, see +`vignette("VaccineEfficacy", package = "gsDesign")`. We focus on exact binomial monitoring with spending functions: -- analyses after each season with spending times `1/3, 2/3, 1`, +- analyses after each season with equally spaced spending times, - efficacy spending based on `sfHSD` with `gamma = 1` (Pocock-like), - exact binomial p-values from `repeatedPValueBinomialExact()` and `sequentialPValueBinomialExact()`. @@ -71,18 +73,32 @@ hr0 <- 1 - ve0 hr1 <- 1 - ve1 seasonal_event_rate_control <- 0.003 -season_length_years <- 0.5 +season_length_months <- 6 +season_length_years <- season_length_months / 12 dropout_6mo <- 0.10 -timing <- c(1 / 3, 2 / 3, 1) +n_seasons <- 3 +stopifnot(n_seasons >= 2) +timing <- seq_len(n_seasons) / n_seasons + +enrollment_months <- 2 +off_enrollment_months <- 10 +annual_cycle_months <- enrollment_months + off_enrollment_months +enroll_pattern <- c(rep(c(1, 0), n_seasons - 1), 1) +enroll_periods <- c(rep(c(enrollment_months, off_enrollment_months), n_seasons - 1), enrollment_months) +calendar_time <- enrollment_months + season_length_months + + annual_cycle_months * (seq_len(n_seasons) - 1) + +test_lower <- rep(FALSE, n_seasons) +test_lower[1] <- TRUE ``` For the exact binomial approximation, the probability that an event is in the experimental group is ```{r} -p_event_experimental <- function(ve, ratio = 3) { - ratio / (ratio + 1 / (1 - ve)) +p_event_experimental <- function(ve, randomization_ratio) { + randomization_ratio / (randomization_ratio + 1 / (1 - ve)) } p0 <- p_event_experimental(ve0, ratio) @@ -92,56 +108,68 @@ c(p0 = p0, p1 = p1) ## Initial group sequential setup -To define spending-function monitoring, we first construct a time-to-event -`gsSurv` object and then convert to integer event looks and exact binomial -bounds. Here we use three looks with timing fractions `1/3, 2/3, 1`. -Enrollment is modeled as 2 months on and 10 months off each year for 3 years. +To define spending-function monitoring, we first construct a calendar-time +`gsSurvCalendar` object and then convert to integer event looks and exact +binomial bounds. Here we use `r n_seasons` looks at months +`r paste(calendar_time, collapse = ", ")`, corresponding to the end of each +season after that year's enrollment period. +Enrollment is modeled as `r enrollment_months` months on and +`r off_enrollment_months` months off each year for `r n_seasons` years. +The `gsSurvCalendar` inputs use months as the time unit, so seasonal event and +dropout probabilities are converted to monthly hazards before fitting the +design. Since events are seasonal, the control event hazard is modeled as +piecewise constant: positive for the `r season_length_months`-month high-risk +season and zero afterward. ```{r} -lambdaC <- -log(1 - seasonal_event_rate_control) / season_length_years -eta <- -log(1 - dropout_6mo) / season_length_years -enroll_pattern <- c(1, 0, 1, 0, 1, 0) -enroll_periods <- c(2, 10, 2, 10, 2, 10) - -design_tte <- gsSurv( - k = 3, # Number of analyses (3 seasons) - test.type = 4, # Non-binding lower bound framework - alpha = alpha, # One-sided Type I error - beta = beta, # Type II error - timing = timing[1:2], # Interim information fractions (season 1 and 2) - sfu = sfHSD, # Efficacy spending function - sfupar = 1, # Pocock-like efficacy spending - sfl = sfHSD, # Futility spending function - sflpar = -2, # Futility spending parameter - lambdaC = lambdaC, # Control hazard rate during high-risk season - hr = hr1, # Alternative hypothesis HR - hr0 = hr0, # Null hypothesis HR - eta = eta, # Dropout hazard rate - gamma = enroll_pattern, # Relative enrollment rates by period - R = enroll_periods, # Period durations (2 months on, 10 months off) - T = 42, # Trial duration in months - minfup = 6, # Minimum follow-up for final enrollees - ratio = ratio, # Experimental:control randomization ratio - testLower = c(TRUE, FALSE, FALSE) # Futility only at IA1 -) |> - toInteger() +lambdaC <- c(-log(1 - seasonal_event_rate_control) / season_length_months, 0) +S <- season_length_months +eta <- -log(1 - dropout_6mo) / season_length_months + +design_calendar <- gsSurvCalendar( + test.type = 4, # Non-binding lower bound framework + alpha = alpha, # One-sided Type I error + beta = beta, # Type II error + calendarTime = calendar_time, # Analysis times in months + spending = "information", # Spending by information fraction + sfu = sfHSD, # Efficacy spending function + sfupar = 1, # Pocock-like efficacy spending + sfl = sfHSD, # Futility spending function + sflpar = -2, # Futility spending parameter + lambdaC = lambdaC, # Control hazard rate per month by period + S = S, # Event-rate period duration in months + hr = hr1, # Alternative hypothesis HR + hr0 = hr0, # Null hypothesis HR + eta = eta, # Dropout hazard rate per month + gamma = enroll_pattern, # Relative enrollment rates by period + R = enroll_periods, # Enrollment period durations in months + minfup = season_length_months, # Minimum follow-up for final enrollees + ratio = ratio, # Experimental:control randomization ratio + testLower = test_lower # Futility only at selected analyses +) + +# Integer conversion can slightly adjust total enrollment so the final integer +# event target is achievable with the seasonal piecewise event-rate model. +design_tte <- suppressWarnings(toInteger(design_calendar)) gsBoundSummary(design_tte) planned_final_events <- design_tte$n.I[design_tte$k] planned_counts <- as.integer(round(planned_final_events * timing)) -planned_counts[3] <- planned_final_events -planned_counts[2] <- max(planned_counts[2], planned_counts[1] + 1L) -planned_counts[3] <- max(planned_counts[3], planned_counts[2] + 1L) +planned_counts[n_seasons] <- planned_final_events +for (j in seq_along(planned_counts)[-1]) { + planned_counts[j] <- max(planned_counts[j], planned_counts[j - 1] + 1L) +} design_exact <- toBinomialExact(design_tte, observedEvents = planned_counts) -planned_cum_enrollment <- as.integer(round(rowSums(design_tte$eNC + design_tte$eNE))) -planned_enrollment_period <- as.numeric(rowSums(as.matrix(design_tte$gamma))) * enroll_periods -season_id <- rep(1:3, each = 2) +planned_enrollment_period <- as.numeric(rowSums(as.matrix(design_tte$gamma))) * + as.numeric(design_tte$R) +season_id <- rep(seq_len(n_seasons), each = 2, length.out = length(planned_enrollment_period)) planned_enrollment_by_season <- as.integer(round(tapply(planned_enrollment_period, season_id, sum))) planned_enrollment_control <- as.integer(round(planned_enrollment_by_season / (1 + ratio))) planned_enrollment_experimental <- planned_enrollment_by_season - planned_enrollment_control +planned_cum_enrollment <- cumsum(planned_enrollment_by_season) ``` The table above is the initial survival-design approximation and includes @@ -203,7 +231,7 @@ nominal_p_futility[futility_active] <- stats::pbinom( ) tibble( - Season = 1:3, + Season = seq_len(n_seasons), `Spending time` = timing, `Planned total events` = design_exact$n.I, `Approx cumulative enrollment` = planned_cum_enrollment, @@ -259,7 +287,7 @@ tibble( locations = cells_column_labels(columns = `Exact efficacy bound (x <= a)`) ) |> tab_footnote( - footnote = "Futility is specified only at IA1; blank futility entries indicate no futility stopping boundary at that analysis.", + footnote = "Blank futility entries indicate no futility stopping boundary at that analysis.", locations = cells_column_labels(columns = `Exact futility bound (x >= b)`) ) ``` @@ -268,7 +296,7 @@ The next table gives planned enrollment/sample size by season and overall. ```{r} enrollment_table <- tibble( - Season = as.character(1:3), + Season = as.character(seq_len(n_seasons)), `Control planned enrollment` = planned_enrollment_control, `Experimental planned enrollment` = planned_enrollment_experimental ) |> @@ -293,14 +321,15 @@ dplyr::bind_rows( ## Example repeated and sequential p-values -Suppose observed event totals at the three seasonal analyses are equal to the -planned totals and observed experimental events are as below. +Suppose observed event totals at the `r n_seasons` seasonal analyses are equal +to the planned totals and observed experimental events are as below. Here, `x` is the observed number of events in the experimental arm. The offset sets season 2 to be one event above the efficacy bound to demonstrate that the repeated p-value is greater than 0.025. ```{r} -x_offset_from_efficacy <- c(0L, 1L, 0L) +x_offset_from_efficacy <- rep(0L, n_seasons) +x_offset_from_efficacy[min(2L, n_seasons)] <- 1L example_x <- pmax(0L, design_exact$lower$bound + x_offset_from_efficacy) example_p <- repeatedPValueBinomialExact( gsD = design_tte, @@ -337,15 +366,14 @@ and leaves earlier looks unchanged. For explicit control, you can pass `usTime` (and for `test.type = 4`, `lsTime`) directly to `toBinomialExact()`, following the same spending-time conventions as -`gsDesign()` and `gsSurv()`. +`gsDesign()`, `gsSurv()`, and `gsSurvCalendar()`. As above, setting `x` one event above an updated efficacy bound at a look gives a repeated p-value above 0.025 for that analysis. ```{r} observed_counts_update <- c( - planned_counts[1], - planned_counts[2], - max(planned_counts[2] + 1L, planned_counts[3] - 5L) + planned_counts[-n_seasons], + max(planned_counts[n_seasons - 1] + 1L, planned_counts[n_seasons] - 5L) ) update_exact <- toBinomialExact(design_tte, observedEvents = observed_counts_update) update_exact_full <- toBinomialExact( @@ -374,14 +402,19 @@ blinded-adaptive seasonal scenarios. In this vignette, we set even when the final observed total event count is below plan. ```{r} +ve_scenarios <- c(`H0 (VE=30%)` = ve0, `H1 (VE=80%)` = ve1) +planned_control_event_rates <- rep(seasonal_event_rate_control, length(ve_scenarios)) + sim_light <- simBinomialSeasonalExact( gsD = design_tte, - ve = c(`H0 (VE=30%)` = ve0, `H1 (VE=80%)` = ve1), - nsim = c(150, 150), - control_event_rate = c(0.003, 0.003), + ve = ve_scenarios, + nsim = rep(150, length(ve_scenarios)), + control_event_rate = planned_control_event_rates, season_length = season_length_years, dropout_rate = dropout_6mo, planned_counts = planned_counts, + enroll_control_per_look = planned_enrollment_control, + enroll_experimental_per_look = planned_enrollment_experimental, adaptive = c(FALSE, TRUE), max_multiplier = 2, final_full_spending = TRUE, @@ -422,18 +455,22 @@ oc |> ## Example with lower-than-planned event rates To illustrate adaptation when events are lower than planned, we halve the -seasonal control event rate from 0.3% to 0.15% and compare fixed versus -adaptive monitoring for both `VE = 30%` and `VE = 80%`. +seasonal control event rate and compare fixed versus adaptive monitoring for +both `VE = 30%` and `VE = 80%`. ```{r} +low_control_event_rates <- planned_control_event_rates / 2 + sim_low <- simBinomialSeasonalExact( gsD = design_tte, - ve = c(`H0 (VE=30%)` = ve0, `H1 (VE=80%)` = ve1), - nsim = c(300, 300), - control_event_rate = c(0.0015, 0.0015), + ve = ve_scenarios, + nsim = rep(300, length(ve_scenarios)), + control_event_rate = low_control_event_rates, season_length = season_length_years, dropout_rate = dropout_6mo, planned_counts = planned_counts, + enroll_control_per_look = planned_enrollment_control, + enroll_experimental_per_look = planned_enrollment_experimental, adaptive = c(FALSE, TRUE), max_multiplier = 2, final_full_spending = TRUE, @@ -509,10 +546,12 @@ sim_type1_big <- simBinomialSeasonalExact( gsD = design_tte, ve = c(`H0 (VE=30%)` = ve0), nsim = type1_nsim, - control_event_rate = 0.003, + control_event_rate = seasonal_event_rate_control, season_length = season_length_years, dropout_rate = dropout_6mo, planned_counts = planned_counts, + enroll_control_per_look = planned_enrollment_control, + enroll_experimental_per_look = planned_enrollment_experimental, adaptive = c(FALSE, TRUE), final_full_spending = TRUE, seed = 5001 @@ -522,10 +561,12 @@ sim_power_big <- simBinomialSeasonalExact( gsD = design_tte, ve = c(`H1 (VE=80%)` = ve1), nsim = power_nsim, - control_event_rate = 0.003, + control_event_rate = seasonal_event_rate_control, season_length = season_length_years, dropout_rate = dropout_6mo, planned_counts = planned_counts, + enroll_control_per_look = planned_enrollment_control, + enroll_experimental_per_look = planned_enrollment_experimental, adaptive = c(FALSE, TRUE), final_full_spending = TRUE, seed = 6001 @@ -536,7 +577,8 @@ sim_power_big <- simBinomialSeasonalExact( - The adaptive pathway shown here is blinded because it uses total event counts only; treatment-group differences are not used to update enrollment. -- Spending fractions are kept fixed at `1/3, 2/3, 1`. +- Spending fractions are set by `timing`; with the current specification they + are `r paste(round(timing, 3), collapse = ", ")`. - For modified intention-to-treat analyses, additional exclusion/dropout mechanisms can be layered into the simulation by reducing at-risk counts before each seasonal event/dropout draw. diff --git a/vignettes/VaccineEfficacy.Rmd b/vignettes/VaccineEfficacy.Rmd index 04d5d30b..beb7ae99 100644 --- a/vignettes/VaccineEfficacy.Rmd +++ b/vignettes/VaccineEfficacy.Rmd @@ -35,6 +35,7 @@ This allows use of spending functions to derive boundaries for the exact method. The time-to-event design can not only be used to set boundaries for the @ChanBohidar method, but to allow specification of enrollment duration and study duration to determine enrollment rates and sample size required. This vignette also illustrates the concept of super-superiority often used in prevention studies. Finally, since this procedure is new as of November, 2023 we suggest checks and potential revisions to spending function choices to optimize design boundaries. +For an extension to annual seasonal monitoring with blinded information-adaptive enrollment, see `vignette("MultiSeasonRareEvents", package = "gsDesign")`. ## Parameterization diff --git a/vignettes/toInteger.Rmd b/vignettes/toInteger.Rmd index d987c3cf..a8da7488 100644 --- a/vignettes/toInteger.Rmd +++ b/vignettes/toInteger.Rmd @@ -13,18 +13,15 @@ vignette: > The **gsDesign** package was originally designed to have continuous sample size planned rather than integer-based sample size. Designs with time-to-event outcomes also had non-integer event counts at times of analysis. -This vignette documents the capability to convert to integer sample sizes and event counts. +This vignette documents the capability to convert to integer sample sizes and event counts. This has a couple of implications on design characteristics: - Information fraction on output will not be exactly as input due to rounding. - Power on output will not be exactly as input. This document goes through examples to demonstrate the calculations. -The new function as of July 2023 is the `toInteger()` which operates on group sequential designs to convert to integer-based total sample size and event counts at analyses. -As of November 2024, the rounding defaults are changing as documented below. -We begin with a summary of the method. -Then we provide an abbreviated example for a time-to-event endpoint design to demonstrate basic concepts. -We follow with a more extended example for a binary endpoint to explain more details. +We begin with a summary of the current method, then use binomial endpoint designs to introduce the basic sample-size rounding behavior. +The final section covers time-to-event designs, where event counts and enrollment are both converted to integer-compatible plans. ## Summary of method @@ -32,28 +29,16 @@ We follow with a more extended example for a binary endpoint to explain more det library(gsDesign) ``` -```{r} -x <- gsSurv(ratio = 1, hr = .74) -y <- x |> toInteger() -# Continuous event counts -x$n.I -# Event counts rounded down -y$n.I -# Continuous sample size at interim and final analyses -as.numeric(x$eNE + x$eNC) -# Rounded up to even final sample size given that x$ratio = 1 -# and rounding to multiple of x$ratio + 1 -as.numeric(y$eNE + y$eNC) -# With roundUpFinal = FALSE, final sample size rounded to nearest integer -z <- x |> toInteger(roundUpFinal = FALSE) -as.numeric(z$eNE + z$eNC) -``` - - When `ratio` is a positive integer, the final sample size is rounded to a multiple of `ratio + 1`. - For 1:1 randomization (experimental:control), set `ratio = 1` to round to an even sample size. - For 2:1 randomization, set `ratio = 2` to round to a multiple of 3. - For 3:2 randomization, set `ratio = 4` to round to a multiple of 5. - Note that for the final analysis the sample size is rounded up to the nearest multiple of `ratio + 1` when `roundUpFinal = TRUE` is specified. If `roundUpFinal = FALSE`, the final sample size is rounded to the nearest multiple of `ratio + 1`. +- For a `gsSurv` object, `n.I` is an event-count schedule rather than a sample-size schedule. + Interim event counts are rounded to the nearest integer, while the final event count is rounded up when `roundUpFinal = TRUE`. +- Survival-design sample size is rounded separately to an allocation-compatible total sample size. + In seasonal or piecewise event-rate designs, `toInteger()` may adjust that rounded sample size by allocation multiples, with a warning, when needed to make the final integer event target achievable. + See `vignette("MultiSeasonRareEvents", package = "gsDesign")` for a complete seasonal exact-binomial monitoring workflow. ## Binomial endpoint designs @@ -65,7 +50,7 @@ We present a simple example based on comparing binomial rates with interim analy n.fix <- nBinomial(p1 = .2, p2 = .1, alpha = .025, beta = .2, ratio = 2) n.fix ``` -If we replace the `beta` argument above with a integer sample size that is a multiple of 3 so that we get the desired 2:1 integer sample sizes per arm (432 = 144 control + 288 experimental targeted) we get slightly larger than the targeted 80% power: +If we replace the `beta` argument above with an integer sample size that is a multiple of 3 so that we get the desired 2:1 integer sample sizes per arm (432 = 144 control + 288 experimental targeted) we get slightly larger than the targeted 80% power: ```{r} nBinomial(p1 = .2, p2 = .1, alpha = .025, n = 432, ratio = 2) @@ -73,7 +58,7 @@ nBinomial(p1 = .2, p2 = .1, alpha = .025, n = 432, ratio = 2) ### 1-sided design -Now we convert the fixed sample size `n.fix` from above to a 1-sided group sequential design with interims after 50% and 75% of observations. Again, sample size at each analysis is not an integer. We use the Lan-DeMets spending function approximating an O'Brien-Fleming efficacy bound. +Now we convert the fixed sample size `n.fix` from above to a 1-sided group sequential design with interims after 50% and 75% of observations. Again, sample size at each analysis is not an integer. We use the Lan-DeMets spending function approximating an O'Brien-Fleming efficacy bound. ```{r} # 1-sided design (efficacy bound only; test.type = 1) @@ -123,7 +108,7 @@ xb$upper$sf(alpha = xb$alpha, t = xb$timing, xb$upper$param)$spend cumsum(x_integer$upper$prob[, 1]) # Specified spending based on the spending function # Slightly different from continuous design due to slightly different information fraction -x$upper$sf(alpha = x_integer$alpha, t = x_integer$timing, x_integer$upper$param)$spend +x_integer$upper$sf(alpha = x_integer$alpha, t = x_integer$timing, x_integer$upper$param)$spend ``` Finally, we look at cumulative boundary crossing probabilities under the alternate hypothesis for each design. Due to rounding up the final sample size, the integer-based design has slightly higher total power than the specified 80% (Type II error `beta = 0.2.`). @@ -198,4 +183,140 @@ The $\beta$-spending lower than 0.2 in the first row above is due to the final s sum(xnbi$upper$prob[, 2]) ``` +## Time-to-event endpoint designs + +For a `gsSurv` object, `n.I` is the event-count schedule. +The `toInteger()` algorithm first converts event counts to integers. +Interim event counts are rounded to the nearest integer. +The final event count is rounded up when `roundUpFinal = TRUE`; otherwise, it is rounded to the nearest integer. +Values within 0.01 of an integer are rounded to that integer, and the resulting sequence is forced to be positive and strictly increasing. +The group sequential design is then recomputed with `gsDesign()` at the integer event counts so the bounds, information fractions, and crossing probabilities reflect the integer schedule. + +Total sample size is handled separately. +The final expected enrollment is rounded to a multiple of `ratio + 1`, rounded up by default or to the nearest multiple if `roundUpFinal = FALSE`. +Enrollment rates are scaled to achieve that rounded sample size over the original calendar plan. +If that normally rounded sample size cannot support the final integer event target, `toInteger()` increases or decreases the final sample size by allocation multiples until the event target is achievable, with a warning. +Finally, the survival quantities are rebuilt: the final analysis time is solved to match the final integer event target, and the interim calendar times are solved to match the integer-design information fractions. + +```{r} +x <- gsSurv(ratio = 1, hr = .74) +y <- x |> toInteger() +# Continuous event counts +x$n.I +# Event counts converted to integers +y$n.I +# Continuous sample size at interim and final analyses +as.numeric(x$eNE + x$eNC) +# Rounded up to even final sample size given that x$ratio = 1 +# and rounding to multiple of x$ratio + 1 +as.numeric(y$eNE + y$eNC) +# With roundUpFinal = FALSE, final sample size rounded to nearest integer +z <- x |> toInteger(roundUpFinal = FALSE) +as.numeric(z$eNE + z$eNC) +``` + +### Seasonal design with a final zero event-rate period + +Seasonal rare-event designs often use piecewise event rates with nonzero rates during seasons and zero rates outside seasons. +The example below has three 6-month seasons separated by off-season zero event-rate periods; the final piecewise event-rate period is zero. +This is the setting where it is especially important that `roundUpFinal = TRUE` applies to the final event count. +For a complete seasonal exact-binomial monitoring workflow, see `vignette("MultiSeasonRareEvents", package = "gsDesign")`. + +```{r} +seasonal_event_rate_control <- 0.003 +season_length <- 0.5 +dropout_6mo <- 0.10 + +season_rate <- -log(1 - seasonal_event_rate_control) / season_length +dropout_rate <- -log(1 - dropout_6mo) / season_length + +seasonal_design <- gsSurv( + k = 3, + test.type = 4, + alpha = 0.025, + beta = 0.1, + timing = c(1 / 3, 2 / 3), + sfu = sfHSD, + sfupar = 1, + sfl = sfHSD, + sflpar = -2, + lambdaC = c(season_rate, 0, season_rate, 0, season_rate, 0), + S = c(6, 6, 6, 6, 6), + hr = 0.2, + hr0 = 0.7, + eta = dropout_rate, + gamma = c(1, 0, 1, 0, 1, 0), + R = c(2, 10, 2, 10, 2, 10), + T = 42, + minfup = 6, + ratio = 3, + testLower = c(TRUE, FALSE, FALSE) +) + +# Continuous event targets and expected enrollment before integer conversion +seasonal_design$n.I +rowSums(seasonal_design$eNC + seasonal_design$eNE) +seasonal_design$T +``` + +The final continuous event target is just under 36 events, so with the default `roundUpFinal = TRUE` the integer event target is 36. +The final expected sample size is just over 4099, which rounds up to 4100 for 3:1 randomization. +For this design, the rounded sample size supports the rounded-up final event target, so no sample-size feasibility warning is needed. +The final analysis time moves later because the rounded-up event target requires additional follow-up under the late zero event-rate period. + +```{r} +seasonal_integer <- toInteger(seasonal_design) + +data.frame( + analysis = seq_len(seasonal_design$k), + continuous_events = round(seasonal_design$n.I, 3), + integer_events = seasonal_integer$n.I, + continuous_time = round(seasonal_design$T, 3), + integer_time = round(seasonal_integer$T, 3), + integer_enrollment = round(rowSums(seasonal_integer$eNC + seasonal_integer$eNE), 3) +) + +# Final integer sample size remains a multiple of ratio + 1. +rowSums(seasonal_integer$eNC + seasonal_integer$eNE)[seasonal_integer$k] +``` + +The important point is that final event-count rounding is not interchangeable with final sample-size rounding. +Integer event targets define the information schedule; the enrollment plan and analysis times are then adjusted only as much as needed to keep those integer event targets attainable under the piecewise event-rate model. + +The feasibility adjustment is easier to see in a rare-event design where the rounded-up event target is too high for the initially rounded sample size. +In the next example, the final event target rounds up to 43, but the rounded final sample size of 5038 cannot ever produce that many expected events. +`toInteger()` increases final sample size by allocation multiples and warns. + +```{r} +rare_design <- gsSurv( + k = 3, + test.type = 4, + alpha = 0.025, + beta = 0.1, + timing = c(1 / 3, 2 / 3), + sfu = sfHSD, + sfupar = 1, + sfl = sfHSD, + sflpar = -2, + lambdaC = -log(1 - 0.0015) / 0.5, + hr = 0.2, + hr0 = 0.7, + eta = dropout_rate, + gamma = c(1, 0, 1, 0, 1, 0), + R = c(2, 10, 2, 10, 2, 10), + T = 42, + minfup = 6, + ratio = 1 +) + +rare_integer <- toInteger(rare_design) + +data.frame( + continuous_final_events = rare_design$n.I[rare_design$k], + integer_final_events = rare_integer$n.I[rare_integer$k], + continuous_final_enrollment = rowSums(rare_design$eNC + rare_design$eNE)[rare_design$k], + integer_final_enrollment = rowSums(rare_integer$eNC + rare_integer$eNE)[rare_integer$k] +) +``` + ## References From 2e9260bd6481725c1f811bd2ce959384feeed759 Mon Sep 17 00:00:00 2001 From: keaven Date: Mon, 25 May 2026 14:55:49 -0400 Subject: [PATCH 08/23] Use integer calendar design in multi-season vignette --- vignettes/MultiSeasonRareEvents.Rmd | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/vignettes/MultiSeasonRareEvents.Rmd b/vignettes/MultiSeasonRareEvents.Rmd index 66bb1ba9..2e05ea04 100644 --- a/vignettes/MultiSeasonRareEvents.Rmd +++ b/vignettes/MultiSeasonRareEvents.Rmd @@ -126,6 +126,8 @@ lambdaC <- c(-log(1 - seasonal_event_rate_control) / season_length_months, 0) S <- season_length_months eta <- -log(1 - dropout_6mo) / season_length_months +# Integer conversion can slightly adjust total enrollment so the final integer +# event target is achievable with the seasonal piecewise event-rate model. design_calendar <- gsSurvCalendar( test.type = 4, # Non-binding lower bound framework alpha = alpha, # One-sided Type I error @@ -146,25 +148,23 @@ design_calendar <- gsSurvCalendar( minfup = season_length_months, # Minimum follow-up for final enrollees ratio = ratio, # Experimental:control randomization ratio testLower = test_lower # Futility only at selected analyses -) - -# Integer conversion can slightly adjust total enrollment so the final integer -# event target is achievable with the seasonal piecewise event-rate model. -design_tte <- suppressWarnings(toInteger(design_calendar)) +) |> + toInteger() |> + suppressWarnings() -gsBoundSummary(design_tte) +gsBoundSummary(design_calendar) -planned_final_events <- design_tte$n.I[design_tte$k] +planned_final_events <- design_calendar$n.I[design_calendar$k] planned_counts <- as.integer(round(planned_final_events * timing)) planned_counts[n_seasons] <- planned_final_events for (j in seq_along(planned_counts)[-1]) { planned_counts[j] <- max(planned_counts[j], planned_counts[j - 1] + 1L) } -design_exact <- toBinomialExact(design_tte, observedEvents = planned_counts) +design_exact <- toBinomialExact(design_calendar, observedEvents = planned_counts) -planned_enrollment_period <- as.numeric(rowSums(as.matrix(design_tte$gamma))) * - as.numeric(design_tte$R) +planned_enrollment_period <- as.numeric(rowSums(as.matrix(design_calendar$gamma))) * + as.numeric(design_calendar$R) season_id <- rep(seq_len(n_seasons), each = 2, length.out = length(planned_enrollment_period)) planned_enrollment_by_season <- as.integer(round(tapply(planned_enrollment_period, season_id, sum))) planned_enrollment_control <- as.integer(round(planned_enrollment_by_season / (1 + ratio))) @@ -179,10 +179,10 @@ In this table, `x` is the cumulative number of observed events in the experimental arm at a given analysis. ```{r} -target_alpha_spend <- design_tte$upper$sf( +target_alpha_spend <- design_calendar$upper$sf( alpha = alpha, t = timing, - param = design_tte$upper$param + param = design_calendar$upper$param )$spend achieved_alpha_spend <- cumsum( @@ -216,8 +216,8 @@ ve_from_bound <- function(x, n, ratio) { out } -futility_active <- if (!is.null(design_tte$testLower)) { - tl <- design_tte$testLower +futility_active <- if (!is.null(design_calendar$testLower)) { + tl <- design_calendar$testLower if (length(tl) == 1) tl <- rep(tl, design_exact$k) as.logical(tl) } else { @@ -332,7 +332,7 @@ x_offset_from_efficacy <- rep(0L, n_seasons) x_offset_from_efficacy[min(2L, n_seasons)] <- 1L example_x <- pmax(0L, design_exact$lower$bound + x_offset_from_efficacy) example_p <- repeatedPValueBinomialExact( - gsD = design_tte, + gsD = design_calendar, n.I = design_exact$n.I, x = example_x ) @@ -343,7 +343,7 @@ The sequential p-value is the minimum repeated p-value: ```{r} sequentialPValueBinomialExact( - gsD = design_tte, + gsD = design_calendar, n.I = design_exact$n.I, x = example_x ) @@ -375,9 +375,9 @@ observed_counts_update <- c( planned_counts[-n_seasons], max(planned_counts[n_seasons - 1] + 1L, planned_counts[n_seasons] - 5L) ) -update_exact <- toBinomialExact(design_tte, observedEvents = observed_counts_update) +update_exact <- toBinomialExact(design_calendar, observedEvents = observed_counts_update) update_exact_full <- toBinomialExact( - design_tte, + design_calendar, observedEvents = observed_counts_update, maxSpend = TRUE ) @@ -406,7 +406,7 @@ ve_scenarios <- c(`H0 (VE=30%)` = ve0, `H1 (VE=80%)` = ve1) planned_control_event_rates <- rep(seasonal_event_rate_control, length(ve_scenarios)) sim_light <- simBinomialSeasonalExact( - gsD = design_tte, + gsD = design_calendar, ve = ve_scenarios, nsim = rep(150, length(ve_scenarios)), control_event_rate = planned_control_event_rates, @@ -462,7 +462,7 @@ both `VE = 30%` and `VE = 80%`. low_control_event_rates <- planned_control_event_rates / 2 sim_low <- simBinomialSeasonalExact( - gsD = design_tte, + gsD = design_calendar, ve = ve_scenarios, nsim = rep(300, length(ve_scenarios)), control_event_rate = low_control_event_rates, @@ -543,7 +543,7 @@ type1_nsim <- 20000 power_nsim <- 3500 sim_type1_big <- simBinomialSeasonalExact( - gsD = design_tte, + gsD = design_calendar, ve = c(`H0 (VE=30%)` = ve0), nsim = type1_nsim, control_event_rate = seasonal_event_rate_control, @@ -558,7 +558,7 @@ sim_type1_big <- simBinomialSeasonalExact( ) sim_power_big <- simBinomialSeasonalExact( - gsD = design_tte, + gsD = design_calendar, ve = c(`H1 (VE=80%)` = ve1), nsim = power_nsim, control_event_rate = seasonal_event_rate_control, From 1745859376421092f343cc625a1ec3afabc5323c Mon Sep 17 00:00:00 2001 From: keaven Date: Mon, 25 May 2026 15:12:54 -0400 Subject: [PATCH 09/23] Allow null VE scenarios in seasonal simulation --- NEWS.md | 5 +++++ R/simBinomialSeasonalExact.R | 13 ++++++++++--- README.md | 2 +- man/simBinomialSeasonalExact.Rd | 5 ++++- ...-independent-test-simBinomialSeasonalExact.R | 17 ++++++++++++++++- vignettes/SeqDesignSurvival.Rmd | 4 ++-- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1b7397f9..93daee8c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -62,6 +62,11 @@ - `simBinomialSeasonalExact()` now supports `usTime`/`lsTime` inputs and reports futility stopping probabilities (`futility_stop_rate` with `futility_mc_se`) in scenario summaries. +- `simBinomialSeasonalExact()` now accepts `ve = 0` and `ve < 0`, allowing + null-hypothesis (`ve = 0`) and non-inferiority margin (`ve < 0`) scenarios. + Validation now requires only that `ve` values are finite and less than 1. + A feasibility check verifies that the implied experimental-arm event rates + (`control_event_rate * (1 - ve)`) remain in `[0, 1)` (#267). ## Bug fixes diff --git a/R/simBinomialSeasonalExact.R b/R/simBinomialSeasonalExact.R index 83ac0212..00779c26 100644 --- a/R/simBinomialSeasonalExact.R +++ b/R/simBinomialSeasonalExact.R @@ -13,7 +13,10 @@ #' #' @param gsD A `gsSurv` object with `test.type` 1 or 4. #' @param ve Numeric vector of vaccine efficacy (or prevention efficacy) -#' scenarios to simulate. +#' scenarios to simulate. Each value must be finite and less than 1. +#' `ve = 0` corresponds to equal event rates (superiority null); `ve < 0` +#' corresponds to experimental-arm event rates above control (non-inferiority +#' margin or harmful scenarios). #' @param nsim Integer scalar or vector giving the number of simulations per #' element of `ve`. #' @param control_event_rate Numeric scalar or vector with control seasonal @@ -105,8 +108,8 @@ simBinomialSeasonalExact <- function( if (!(gsD$test.type %in% c(1, 4))) { stop("gsD$test.type must be 1 or 4", call. = FALSE) } - if (!is.numeric(ve) || length(ve) < 1 || any(!is.finite(ve)) || any(ve <= 0) || any(ve >= 1)) { - stop("ve must be a numeric vector with values strictly between 0 and 1", call. = FALSE) + if (!is.numeric(ve) || length(ve) < 1 || any(!is.finite(ve)) || any(ve >= 1)) { + stop("ve must be a numeric vector with finite values less than 1", call. = FALSE) } if (!is.numeric(season_length) || length(season_length) != 1 || !is.finite(season_length) || season_length <= 0) { stop("season_length must be a positive scalar", call. = FALSE) @@ -146,6 +149,10 @@ simBinomialSeasonalExact <- function( any(!is.finite(control_event_rate)) || any(control_event_rate <= 0) || any(control_event_rate >= 1)) { stop("control_event_rate must be a scalar or vector in (0, 1) with one value per ve scenario", call. = FALSE) } + experimental_event_rate <- control_event_rate * (1 - ve) + if (any(experimental_event_rate < 0) || any(experimental_event_rate >= 1)) { + stop("ve and control_event_rate imply experimental event rates outside [0, 1)", call. = FALSE) + } if (!is.logical(adaptive) || length(adaptive) < 1 || any(is.na(adaptive))) { stop("adaptive must be a logical vector with at least one value", call. = FALSE) diff --git a/README.md b/README.md index 87732779..42487868 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The gsDesign package supports group sequential clinical trial design, largely as presented in the book Group Sequential Methods with Applications to Clinical Trials by Christopher Jennison and Bruce Turnbull (Chapman and Hall/CRC, 2000). -An easy-to-use [web interface](https://rinpharma.shinyapps.io/gsdesign/) to both enable usage without coding and to generate code to be able to reproduce the design; this is being enhanced to support more features on an ongoing basis. See the [talk](https://www.youtube.com/watch?v=8uZRuvzma9M) presented at the R/Pharma Conference to get started with the web interface. +An easy-to-use [web interface](https://rinpharma.shinyapps.io/gsdesign/) to both enable usage without coding and to generate code to be able to reproduce the design; this is being enhanced to support more features on an ongoing basis. See the [talk](https://www.youtube.com/embed/8uZRuvzma9M) presented at the R/Pharma Conference to get started with the web interface. With improving large language models since late 2025, updates and additions to the gsDesign package have accelerated. ## Installation diff --git a/man/simBinomialSeasonalExact.Rd b/man/simBinomialSeasonalExact.Rd index bcd47000..a509f2ed 100644 --- a/man/simBinomialSeasonalExact.Rd +++ b/man/simBinomialSeasonalExact.Rd @@ -29,7 +29,10 @@ simBinomialSeasonalExact( \item{gsD}{A `gsSurv` object with `test.type` 1 or 4.} \item{ve}{Numeric vector of vaccine efficacy (or prevention efficacy) -scenarios to simulate.} +scenarios to simulate. Each value must be finite and less than 1. +`ve = 0` corresponds to equal event rates (superiority null); `ve < 0` +corresponds to experimental-arm event rates above control (non-inferiority +margin or harmful scenarios).} \item{nsim}{Integer scalar or vector giving the number of simulations per element of `ve`.} diff --git a/tests/testthat/test-independent-test-simBinomialSeasonalExact.R b/tests/testthat/test-independent-test-simBinomialSeasonalExact.R index a4aa6a70..3801b0b9 100644 --- a/tests/testthat/test-independent-test-simBinomialSeasonalExact.R +++ b/tests/testthat/test-independent-test-simBinomialSeasonalExact.R @@ -61,7 +61,22 @@ test_that("simBinomialSeasonalExact validates core inputs", { expect_error( simBinomialSeasonalExact(gsD = design, ve = c(0.3, 1)), - "strictly between 0 and 1" + "finite values less than 1" + ) + expect_no_error( + simBinomialSeasonalExact(gsD = design, ve = c(0, 0.3), nsim = c(1, 1)) + ) + expect_no_error( + simBinomialSeasonalExact( + gsD = design, + ve = c(-0.1, 0, 0.3), + nsim = c(1, 1, 1), + control_event_rate = 0.003 + ) + ) + expect_error( + simBinomialSeasonalExact(gsD = design, ve = -10, nsim = 1, control_event_rate = 0.5), + "experimental event rates outside" ) expect_error( diff --git a/vignettes/SeqDesignSurvival.Rmd b/vignettes/SeqDesignSurvival.Rmd index 8e85cb19..d1c1f590 100644 --- a/vignettes/SeqDesignSurvival.Rmd +++ b/vignettes/SeqDesignSurvival.Rmd @@ -27,10 +27,10 @@ This vignette provides a thorough analysis of the differences between the SAS SEQDESIGN procedure and the R package gsDesign for designing group sequential clinical trials with time-to-event endpoints. -### Starting point: SAS PROC SEQDESIGN Example 109.14 +### Starting point: SAS PROC SEQDESIGN survival example Consider the example described in the SAS Documentation: -[Computing Sample Size for Survival Data with Uniform Accrual](https://documentation.sas.com/doc/en/statug/15.2/statug_seqdesign_examples14.htm). +[Computing Sample Size for Survival Data with Uniform Accrual](https://support.sas.com/documentation/cdl/en/statug/68162/HTML/default/statug_seqdesign_examples14.htm). The SAS code is as follows: ```sas From 4372fe2ed5c0a9febfb9e93ba32152761c7f5eb7 Mon Sep 17 00:00:00 2001 From: keaven Date: Mon, 25 May 2026 20:42:50 -0400 Subject: [PATCH 10/23] Align survival timing solves with SAS --- NEWS.md | 192 ++++++++------- R/gsSurv-method.R | 108 +++++++++ R/gsSurv-nSurv.R | 32 ++- R/gsSurv.R | 62 ++++- man/nSurv.Rd | 18 +- tests/testthat/test-nSurv.R | 108 +++++++++ vignettes/SeqDesignSurvival.Rmd | 404 +++++++++++++++++++++++++------- 7 files changed, 739 insertions(+), 185 deletions(-) diff --git a/NEWS.md b/NEWS.md index 93daee8c..d8868052 100644 --- a/NEWS.md +++ b/NEWS.md @@ -46,22 +46,23 @@ final spending fraction to 1 when desired. This makes it easier to evaluate delayed event accrual while keeping spending tied to a planned information schedule. It also preserves the original one-sided versus two-sided design - convention when inheriting defaults from an existing `gsSurv` object. + convention when inheriting defaults from an existing `gsSurv` object (#258). - New vignette "Power Computation for Group Sequential Survival Designs" (`vignette("gsSurvPower")`) with worked examples for sensitivity analysis, alpha reallocation, biomarker subgroup to stratified design, and event-driven timing (@keaven, #109). - Added `repeatedPValueBinomialExact()` and `sequentialPValueBinomialExact()` to compute repeated and sequential exact-binomial p-values under spending - function designs derived from `gsSurv()` objects. + function designs derived from `gsSurv()` objects (1922429e). - Added `simBinomialSeasonalExact()` to run fixed and blinded-adaptive seasonal - rare-event simulations with exact-binomial efficacy monitoring summaries. + rare-event simulations with exact-binomial efficacy monitoring summaries + (1922429e). - `toBinomialExact()` now supports explicit spending-time overrides via `usTime` and `lsTime` (for `test.type = 4`) to align with `gsDesign()` and - `gsSurv()` conventions when updating bounds with `observedEvents`. + `gsSurv()` conventions when updating bounds with `observedEvents` (1922429e). - `simBinomialSeasonalExact()` now supports `usTime`/`lsTime` inputs and reports futility stopping probabilities (`futility_stop_rate` with - `futility_mc_se`) in scenario summaries. + `futility_mc_se`) in scenario summaries (1922429e). - `simBinomialSeasonalExact()` now accepts `ve = 0` and `ve < 0`, allowing null-hypothesis (`ve = 0`) and non-inferiority margin (`ve < 0`) scenarios. Validation now requires only that `ve` values are finite and less than 1. @@ -70,6 +71,12 @@ ## Bug fixes +- `nSurv()` and `gsSurv()` now use the requested survival sample size method + when either `T` or `minfup` is `NULL`. `gsSurv()` also uses the input + accrual rate and duration when both `T` and `minfup` are `NULL`, solving + follow-up duration against the final group-sequential event requirement. + This allows Schoenfeld survival designs to reproduce SAS PROC SEQDESIGN's + fixed-accrual follow-up solve (#265). - `simBinomialSeasonalExact()` now stops simulated trials at the first efficacy or futility boundary crossing for reporting stopping time, total events, and total enrollment, while preserving the non-binding futility @@ -81,7 +88,8 @@ `testHarm`) and harm-bound spending (`sfharm`, `sfharmparam` for `test.type` 7 or 8) when recomputing the design after integer sample size or event-count rounding. Previously the internal `gsDesign()` call - omitted these settings, so inactive looks could incorrectly become active. + omitted these settings, so inactive looks could incorrectly become active + (#261). - `toInteger()` now preserves the intended survival-design behavior that `roundUpFinal = TRUE` rounds the final event count up. If the independently rounded final sample size, using the usual `ratio + 1` allocation multiple, @@ -94,9 +102,10 @@ conversion. Both now use `sign(hr1 - hr0)` (@keaven, #251). - Fixed `toBinomialExact()` one-sided (`test.type = 1`) updating with `observedEvents` so futility-adjustment code is only executed when - `test.type = 4`. + `test.type = 4` (1922429e). - `toBinomialExact()` now respects selective futility testing (`testLower`) when - present on a `gsSurv` object by flattening lower spending at inactive looks. + present on a `gsSurv` object by flattening lower spending at inactive looks + (1922429e). ## Documentation @@ -121,51 +130,61 @@ - Expanded `gsSurvPower()` documentation and vignette guidance for `informationRates`, calendar spending, and `fullSpendingAtFinal`, including a corrected worked example of the spending fractions used at the final - analysis. + analysis (#258). +- Clarified the PROC SEQDESIGN survival vignette comparison by using + `test.type = 2`, `alpha = 0.025`, `method = "Schoenfeld"`, and + `T = minfup = NULL` to match SAS's symmetric two-sided fixed-accrual + design, and by separating fractional-time output from the SAS ceiling-time + adjusted design (#265). - Added vignette "Multi-season studies for rare events" (`vignette("MultiSeasonRareEvents")`) demonstrating exact-binomial seasonal monitoring, analysis-time bound updates via `toBinomialExact(observedEvents = ...)`, and blinded information-adaptive - enrollment scenarios. + enrollment scenarios (1922429e). - Expanded the multi-season vignette with: initial `gsBoundSummary()` output, IA1-only futility illustration, VE and nominal one-sided p-values at exact-binomial bounds, and clearer simulation tables including efficacy and - futility stopping probabilities with non-binding Type I interpretation notes. + futility stopping probabilities with non-binding Type I interpretation notes + (c1065ea8, 2e9260bd). - Reorganized pkgdown article sections to separate general materials, exact - binomial workflows, and multiple-hypothesis-testing content. + binomial workflows, and multiple-hypothesis-testing content (67146132). - Added repository-level coding agent instructions for Codex, Claude Code, Gemini, and GitHub Copilot to support consistent gsDesign issue-branch, - testing, NEWS/version, pkgdown, commit, and push workflows. + testing, NEWS/version, pkgdown, commit, and push workflows (04cc7929). - Added vignette "Using gsDesign AI skills" (`vignette("gsDesignAISkills")`) and package-shipped AI workflow templates under `inst/ai/` so users can copy the agent instructions into their own - projects after installing `gsDesign`. + projects after installing `gsDesign` (8dd96c3b). - Expanded the AI instructions and vignette example with function-selection guidance that maps calendar-timed survival-design prompts, such as a 24-month interim after enrollment opens with `ratio = 1`, to `gsSurvCalendar()` while preserving the original design specifications, and clarifies that very low event counts can cue discussion of exact-binomial - rare-event methods without automatically changing the design family. + rare-event methods without automatically changing the design family + (b1eae2f0). ## Testing - Added `toInteger()` regression tests for selective-bound preservation on `gsDesign` and `gsSurv` objects, including `test.type` 8 with custom harm - spending. + spending (#261). - Added focused `gsSurvPower()` regression tests for `informationRates`, `fullSpendingAtFinal`, and inherited sidedness behavior from existing - time-to-event designs. + time-to-event designs (#258). - Added independent tests for exact-binomial repeated/sequential p-values and for `simBinomialSeasonalExact()` input validation, reproducibility, and - adaptive enrollment behavior. + adaptive enrollment behavior (1922429e, c1065ea8). - Added regression test confirming `toBinomialExact()` one-sided - (`test.type = 1`) updates with `observedEvents`. + (`test.type = 1`) updates with `observedEvents` (1922429e). - Added regression tests for `toBinomialExact()` `usTime`/`lsTime` overrides and selective-futility behavior, plus tests for new futility stopping summary - outputs from `simBinomialSeasonalExact()`. + outputs from `simBinomialSeasonalExact()` (1922429e). - Added regression tests for `simBinomialSeasonalExact()` stopping summaries, design-based fixed enrollment defaults, and the rare-event `toInteger()` equal-allocation path (#264). +- Expanded `nSurv()` and `gsSurv()` regression tests across the supported + `T`/`minfup` timing combinations for Schoenfeld, Freedman, and + Bernstein-Lagakos methods (#265). # gsDesign 3.9.0 (February 2026) @@ -287,7 +306,7 @@ - New function `binomialPowerTable()` generates power tables across control rates and treatment effects. Supports both analytical calculations and - fast simulation for exact results. + fast simulation for exact results (c466a616). # gsDesign 3.6.8 (May 2025) @@ -479,21 +498,23 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.5.0 (July 2023) -- `sfPower()` now allows a wider parameter range (0, 15]. +- `sfPower()` now allows a wider parameter range (0, 15] (5502e4a1). - `toInteger()` function added to convert `gsDesign` or `gsSurv` classes - to integer sample size and event counts. + to integer sample size and event counts (5502e4a1). - `toBinomialExact()` function added to convert time-to-event bounds to - exact binomial for low event rate studies. + exact binomial for low event rate studies (5502e4a1). - Added "A Gentle Introduction to Group Sequential Design" vignette for - an introduction to asymptotics for group sequential design. + an introduction to asymptotics for group sequential design (5502e4a1). - `as_table()` and `as_gt()` methods for `gsBinomialExact` objects added, - as described in the new "Binomial SPRT" vignette. + as described in the new "Binomial SPRT" vignette (5502e4a1). - In `plot.ssrCP()`, the `hat` syntax in the mathematical expression is revised, - resolving labeling issues. -- `ggplot2::qplot()` usage replaced due to its deprecation in ggplot2 3.4.0. + resolving labeling issues (5502e4a1). +- `ggplot2::qplot()` usage replaced due to its deprecation in ggplot2 3.4.0 + (5502e4a1). - Link update for the gsDesign manual in the documentation, - now directly pointing to the gsDesign technical manual bookdown project. -- Introduced a new hex sticker logo. + now directly pointing to the gsDesign technical manual bookdown project + (5502e4a1). +- Introduced a new hex sticker logo (5502e4a1). # gsDesign 3.4.0 (October 2022) @@ -507,97 +528,98 @@ We have made the spending function summary output more readable and informative. - Addition of vignettes - Demonstrate cure model and calendar-based analysis timing for time-to-event endpoint design - Vaccine efficacy design using spending bounds and exact binomial boundary crossing probabilities -- Minor fix to labeling in print.gsProbability -- Fixed error in sfStep -- Updates to reduce R CMD check and other minor issues + (85f1875f). +- Minor fix to labeling in print.gsProbability (85f1875f). +- Fixed error in sfStep (85f1875f). +- Updates to reduce R CMD check and other minor issues (85f1875f). # gsDesign 3.2.2 (January 2022) -- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate -- Correctly close graphics device in unit tests to avoid plot output file not found issues -- Minor fixes to hGraph() for multiplicity graphs -- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue -- Minor changes to vignettes +- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate (cbf9a940). +- Correctly close graphics device in unit tests to avoid plot output file not found issues (cbf9a940). +- Minor fixes to hGraph() for multiplicity graphs (cbf9a940). +- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue (cbf9a940). +- Minor changes to vignettes (cbf9a940). # gsDesign 3.2.1 (July 2021) -- Changed gt package usage in a vignette due to deprecated gt function -- Replied to minor comments from CRAN reviewer (no functionality impact) -- Minor update to DESCRIPTION citing Jennison and Turnbull reference +- Changed gt package usage in a vignette due to deprecated gt function (eae74e2b). +- Replied to minor comments from CRAN reviewer (no functionality impact) (eae74e2b). +- Minor update to DESCRIPTION citing Jennison and Turnbull reference (eae74e2b). # gsDesign 3.2.0 (January 2021) -- Substantially updated unit testing to increase code coverage above 80% -- Updated error checking messages to print function where check fails -- Removed dependencies on plyr packages -- Updated github actions +- Substantially updated unit testing to increase code coverage above 80% (96e49fd7). +- Updated error checking messages to print function where check fails (96e49fd7). +- Removed dependencies on plyr packages (96e49fd7). +- Updated github actions (96e49fd7). # gsDesign 3.1.1 (May 2020) -- Vignettes updated -- Added `hGraph()` to support ggplot2 versions of multiplicity graphs -- Eliminated unnecessary check from `sequentialPValue` -- Targeted release to CRAN -- Removed dependencies on reshape2, plyr -- Updated continuous integration -- Updated license +- Vignettes updated (520c23e4). +- Added `hGraph()` to support ggplot2 versions of multiplicity graphs (520c23e4). +- Eliminated unnecessary check from `sequentialPValue` (520c23e4). +- Targeted release to CRAN (520c23e4). +- Removed dependencies on reshape2, plyr (520c23e4). +- Updated continuous integration (520c23e4). +- Updated license (520c23e4). # gsDesign 3.1.0 (April 2019) -- Addition of pkgdown web site -- Updated unit testing to from RUnit to testthat -- Converted to roxygen2 generation of help files -- Converted vignettes to R Markdown -- Added Travis-CI and Appveyor support -- Added `sequentialPValue` function -- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv` +- Addition of pkgdown web site (b4001d4d). +- Updated unit testing to from RUnit to testthat (b4001d4d). +- Converted to roxygen2 generation of help files (b4001d4d). +- Converted vignettes to R Markdown (b4001d4d). +- Added Travis-CI and Appveyor support (b4001d4d). +- Added `sequentialPValue` function (b4001d4d). +- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv` (b4001d4d). # gsDesign 3.0-5 (January 2018) -- Registered C routines -- Fixed "gsbound" -- Replaced "array" by "rep" calls to avoid `R CMD check` warnings +- Registered C routines (e52467d5). +- Fixed "gsbound" (e52467d5). +- Replaced "array" by "rep" calls to avoid `R CMD check` warnings (db229717). # gsDesign 3.0-4 (September 2017) -- First Github-based release -- Cleaned up documentation for `nBinomial1Sample()` -- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity -- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming +- First Github-based release (e52467d5). +- Cleaned up documentation for `nBinomial1Sample()` (e52467d5). +- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity (e52467d5). +- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming (e52467d5). # gsDesign 3.0-3 -- Introduced spending time as a separate concept from information time to enable concepts such as calendar-based spending functions. The only user function changed is the `gsDesign()` function and the change is the addition of the parameters `usTime` and `lsTime`; default behavior is backwards compatible. +- Introduced spending time as a separate concept from information time to enable concepts such as calendar-based spending functions. The only user function changed is the `gsDesign()` function and the change is the addition of the parameters `usTime` and `lsTime`; default behavior is backwards compatible (e52467d5). # gsDesign 3.0-2 (February 2016) -- Simplified conditional power section of gsDesignManual.pdf in doc directory -- Corrected basic calculation in `gsCP()` -- Eliminated deprecated ggplot2 function `opts()` +- Simplified conditional power section of gsDesignManual.pdf in doc directory (e52467d5). +- Corrected basic calculation in `gsCP()` (e52467d5). +- Eliminated deprecated ggplot2 function `opts()` (e52467d5). # gsDesign 3.0-1 (January 2016) -- More changes to comply with R standards (in NAMESPACE - `importFrom` statements - and DESCRIPTION - adding plyr to imports) ensuring appropriate references. -- Deleted link in documentation that no longer exists (gsBinomialExact.Rd). -- Last planned RForge-based release; moving to Github. +- More changes to comply with R standards (in NAMESPACE - `importFrom` statements - and DESCRIPTION - adding plyr to imports) ensuring appropriate references (e52467d5). +- Deleted link in documentation that no longer exists (gsBinomialExact.Rd) (e52467d5). +- Last planned RForge-based release; moving to Github (e52467d5). # gsDesign 3.0-0 (December 2015) -- Updated xtable extension to meet R standards for extensions. -- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs -- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact -- Minor typo fixed in `sfLogistic` help file -- Cleaned up "imports" and "depends" in an effort to be an R "good citizen" -- Registered S3 methods in NAMESPACE +- Updated xtable extension to meet R standards for extensions (e52467d5). +- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs (e52467d5). +- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact (e52467d5). +- Minor typo fixed in `sfLogistic` help file (e52467d5). +- Cleaned up "imports" and "depends" in an effort to be an R "good citizen" (e52467d5). +- Registered S3 methods in NAMESPACE (e52467d5). # gsDesign 2.9-4 -- Minor edit to package description to comply with R standards +- Minor edit to package description to comply with R standards (e52467d5). # gsDesign 2.9-3 (November 2014) -- Added `sfTrimmed` as likely preferred spending function approach to skipping early or all interim efficacy analyses; this also can adjust bound when final analysis is performed with less than maximum planned information. Updated `help(sfTrimmed)` to demonstrate these capabilities. -- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example -- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects -- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file -- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects +- Added `sfTrimmed` as likely preferred spending function approach to skipping early or all interim efficacy analyses; this also can adjust bound when final analysis is performed with less than maximum planned information. Updated `help(sfTrimmed)` to demonstrate these capabilities (e52467d5). +- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example (e52467d5). +- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects (e52467d5). +- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file (e52467d5). +- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects (e52467d5). diff --git a/R/gsSurv-method.R b/R/gsSurv-method.R index 81bdfb11..ac363439 100644 --- a/R/gsSurv-method.R +++ b/R/gsSurv-method.R @@ -267,6 +267,114 @@ LFPWE <- function( return(rval) } +accrual_gamma <- function(gamma, R) { + if (!is.matrix(gamma)) gamma <- matrix(gamma) + if (nrow(gamma) == 1 && length(R) > 1) { + gamma <- gamma[rep(1, length(R)), , drop = FALSE] + } + if (nrow(gamma) > length(R)) { + gamma <- gamma[seq_along(R), , drop = FALSE] + } + if (nrow(gamma) != length(R)) { + stop("gamma must have one row or the same number of rows as R") + } + gamma +} + +accrual_total <- function(gamma, R) { + gamma <- accrual_gamma(gamma, R) + sum(rowSums(gamma) * R) +} + +LFPWESolveAccrualDuration <- function( + alpha = .025, sided = 1, beta = .1, + lambdaC = log(2) / 6, hr = .5, hr0 = 1, etaC = 0, etaE = 0, + gamma = 1, ratio = 1, R = 18, S = NULL, minfup = 6, + tol = .Machine$double.eps^0.25, + method = c("Schoenfeld", "Freedman", "BernsteinLagakos") +) { + method <- match.arg(method) + objective <- function(accrual_duration, simple = TRUE) { + R_search <- R + R_search[length(R_search)] <- Inf + fit <- LFPWE( + alpha = alpha, sided = sided, beta = beta, + lambdaC = lambdaC, hr = hr, hr0 = hr0, etaC = etaC, etaE = etaE, + gamma = gamma, ratio = ratio, R = R_search, S = S, + T = accrual_duration + minfup, minfup = minfup, method = method + ) + target_n <- accrual_total(gamma, fit$R) + if (simple) return(fit$n - target_n) + fit$n <- target_n + fit$gamma <- accrual_gamma(gamma, fit$R) + fit$variable <- "Accrual duration" + fit$tol <- tol + fit + } + + left <- objective(.01) + right <- objective(10000) + if (left < 0) { + stop(paste( + "With T = NULL, trial is over-powered for any accrual duration.", + "Reduce accrual rates (gamma), increase beta, or adjust assumptions." + )) + } + if (right > 0) { + stop(paste( + "With T = NULL, trial is under-powered for any accrual duration.", + "Increase accrual rates (gamma), decrease beta, or adjust assumptions." + )) + } + root <- stats::uniroot(objective, interval = c(.01, 10000), tol = tol)$root + objective(root, simple = FALSE) +} + +LFPWESolveFollowupDuration <- function( + alpha = .025, sided = 1, beta = .1, + lambdaC = log(2) / 6, hr = .5, hr0 = 1, etaC = 0, etaE = 0, + gamma = 1, ratio = 1, R = 18, S = NULL, + tol = .Machine$double.eps^0.25, + method = c("Schoenfeld", "Freedman", "BernsteinLagakos") +) { + method <- match.arg(method) + if (sum(R) == Inf) { + stop("Enrollment duration must be specified as finite") + } + objective <- function(followup, simple = TRUE) { + fit <- LFPWE( + alpha = alpha, sided = sided, beta = beta, + lambdaC = lambdaC, hr = hr, hr0 = hr0, etaC = etaC, etaE = etaE, + gamma = gamma, ratio = ratio, R = R, S = S, + T = sum(R) + followup, minfup = followup, method = method + ) + target_n <- accrual_total(gamma, fit$R) + if (simple) return(fit$n - target_n) + fit$n <- target_n + fit$gamma <- accrual_gamma(gamma, fit$R) + fit$variable <- "Follow-up duration" + fit$tol <- tol + fit + } + + left <- objective(.01) + right <- objective(10000) + if (left < 0) { + stop(paste( + "With minfup = NULL, trial is over-powered for any follow-up duration.", + "Reduce accrual rates (gamma), increase beta, or adjust assumptions." + )) + } + if (right > 0) { + stop(paste( + "With minfup = NULL, trial is under-powered for any follow-up duration.", + "Increase accrual rates (gamma), decrease beta, or adjust assumptions." + )) + } + root <- stats::uniroot(objective, interval = c(.01, 10000), tol = tol)$root + objective(root, simple = FALSE) +} + # KTZ function [sinew] ---- #' @importFrom stats pnorm qnorm KTZ <- function( diff --git a/R/gsSurv-nSurv.R b/R/gsSurv-nSurv.R index 3b316d11..0a1ab4a8 100644 --- a/R/gsSurv-nSurv.R +++ b/R/gsSurv-nSurv.R @@ -72,9 +72,9 @@ #' #' When study duration (\code{T}) and follow-up duration (\code{minfup}) are #' fixed, \code{nSurv} applies exactly the Lachin and Foulkes (1986) method of -#' computing sample size under the proportional hazards assumption when For -#' this computation, enrollment rates are altered proportionately to those -#' input in \code{gamma} to achieve the power of interest. +#' computing sample size under the proportional hazards assumption. For this +#' computation, enrollment rates are altered proportionately to those input in +#' \code{gamma} to achieve the power of interest. #' #' Given the specified enrollment rate(s) input in \code{gamma}, \code{nSurv} #' may also be used to derive enrollment duration required for a trial to have @@ -89,10 +89,20 @@ #' method will fail if the specified enrollment rates and durations either #' over-powers the trial with no additional follow-up or underpowers the trial #' with infinite follow-up. This method produces a corresponding error message -#' in such cases. +#' in such cases. For methods other than Lachin and Foulkes, these fixed-rate +#' duration solves use the selected method for the fixed-design event +#' calculation. #' #' The input to \code{gsSurv} is a combination of the input to \code{nSurv()} #' and \code{gsDesign()}. +#' When \code{T = NULL} and \code{minfup} is specified, \code{gsSurv()} +#' preserves the input accrual rate and minimum follow-up, applies the group +#' sequential design, and solves the accrual duration needed for the final +#' planned number of events. +#' When both \code{T} and \code{minfup} are \code{NULL}, \code{gsSurv()} +#' preserves the input accrual rate and duration, applies the group sequential +#' design, and solves the follow-up duration needed for the final planned +#' number of events. #' #' \code{nEventsIA()} is provided to compute the expected number of events at a #' given point in time given enrollment, event and censoring rates. The routine @@ -505,7 +515,19 @@ nSurv <- function( etaC <- matrix(eta, nrow = nlambda, ncol = nstrata) etaE <- matrix(etaE, nrow = nlambda, ncol = nstrata) if (!is.matrix(gamma)) gamma <- matrix(gamma) - if (is.null(minfup) || is.null(T)) { + if (is.null(minfup) && method != "LachinFoulkes") { + xx <- LFPWESolveFollowupDuration( + lambdaC = lambdaC, hr = hr, hr0 = hr0, etaC = etaC, etaE = etaE, + gamma = gamma, R = R, S = S, ratio = ratio, + alpha = alpha, sided = sided, beta = beta, tol = tol, method = method + ) + } else if (is.null(T) && !is.null(minfup) && method != "LachinFoulkes") { + xx <- LFPWESolveAccrualDuration( + lambdaC = lambdaC, hr = hr, hr0 = hr0, etaC = etaC, etaE = etaE, + gamma = gamma, R = R, S = S, minfup = minfup, ratio = ratio, + alpha = alpha, sided = sided, beta = beta, tol = tol, method = method + ) + } else if (is.null(minfup) || is.null(T)) { xx <- KT( lambdaC = lambdaC, hr = hr, hr0 = hr0, etaC = etaC, etaE = etaE, gamma = gamma, R = R, S = S, minfup = minfup, ratio = ratio, diff --git a/R/gsSurv.R b/R/gsSurv.R index ee2a9815..53ddbe85 100644 --- a/R/gsSurv.R +++ b/R/gsSurv.R @@ -27,9 +27,63 @@ gsSurv <- function( if (!is.numeric(ratio) || length(ratio) != 1 || ratio <= 0) { stop("ratio must be a single positive scalar") } - # If both gamma and R are provided (non-NULL) and T is NULL, set T to force solving for accrual rate - # This matches gsDesign::gsSurv behavior which keeps R fixed and solves for gamma - if (is.null(T) && !is.null(minfup) && + solve_followup <- is.null(T) && is.null(minfup) + if (solve_followup) { + if (is.null(beta)) { + stop("When beta is NULL, T and minfup cannot both be NULL") + } + if (sum(R) == Inf) { + stop("Enrollment duration must be specified as finite") + } + + target_n <- accrual_total(gamma, R) + + fixed_accrual_difference <- function(followup) { + trial_T <- sum(R) + followup + x_candidate <- nSurv( + lambdaC = lambdaC, hr = hr, hr0 = hr0, eta = eta, etaE = etaE, + gamma = gamma, R = R, S = S, T = trial_T, minfup = followup, + ratio = ratio, alpha = alpha, beta = beta, sided = sided, + tol = tol, method = method + ) + y_candidate <- gsDesign( + k = k, test.type = test.type, alpha = alpha / sided, + beta = beta, astar = astar, n.fix = x_candidate$d, + timing = timing, sfu = sfu, sfupar = sfupar, sfl = sfl, + sflpar = sflpar, sfharm = sfharm, sfharmparam = sfharmparam, + tol = tol, delta1 = log(hr), delta0 = log(hr0), + usTime = usTime, lsTime = lsTime, testUpper = testUpper, + testLower = testLower, testHarm = testHarm + ) + z_candidate <- gsnSurv(x_candidate, y_candidate$n.I[k]) + z_candidate$n - target_n + } + + left <- fixed_accrual_difference(.01) + right <- fixed_accrual_difference(10000) + if (left < 0) { + stop(paste( + "With T = NULL and minfup = NULL, trial is over-powered for any", + "follow-up duration. Reduce accrual rates (gamma), increase beta,", + "or adjust assumptions." + )) + } + if (right > 0) { + stop(paste( + "With T = NULL and minfup = NULL, trial is under-powered for any", + "follow-up duration. Increase accrual rates (gamma), decrease beta,", + "or adjust assumptions." + )) + } + + minfup <- stats::uniroot( + fixed_accrual_difference, interval = c(.01, 10000), tol = tol + )$root + T <- sum(R) + minfup + } + # Preserve the historical Lachin-Foulkes default: with fixed follow-up and + # T = NULL, keep R fixed and vary the accrual rate. + if (method == "LachinFoulkes" && is.null(T) && !is.null(minfup) && !is.null(R) && length(R) > 0 && !is.null(gamma) && length(gamma) > 0) { T <- sum(R) + minfup @@ -79,6 +133,7 @@ gsSurv <- function( y$etaC <- z$etaC y$etaE <- z$etaE y$variable <- x$variable + if (solve_followup) y$variable <- "Follow-up duration" y$tol <- tol y$method <- x$method y$call <- match.call() @@ -339,4 +394,3 @@ print.gsSurv <- function(x, digits = 3, show_gsDesign = FALSE, show_strata = TRU invisible(x) } - diff --git a/man/nSurv.Rd b/man/nSurv.Rd index c424a22a..17a4bef1 100644 --- a/man/nSurv.Rd +++ b/man/nSurv.Rd @@ -501,9 +501,9 @@ enrollment rate. When study duration (\code{T}) and follow-up duration (\code{minfup}) are fixed, \code{nSurv} applies exactly the Lachin and Foulkes (1986) method of -computing sample size under the proportional hazards assumption when For -this computation, enrollment rates are altered proportionately to those -input in \code{gamma} to achieve the power of interest. +computing sample size under the proportional hazards assumption. For this +computation, enrollment rates are altered proportionately to those input in +\code{gamma} to achieve the power of interest. Given the specified enrollment rate(s) input in \code{gamma}, \code{nSurv} may also be used to derive enrollment duration required for a trial to have @@ -518,10 +518,20 @@ enrollment duration(s) specified in \code{R} are considered fixed and method will fail if the specified enrollment rates and durations either over-powers the trial with no additional follow-up or underpowers the trial with infinite follow-up. This method produces a corresponding error message -in such cases. +in such cases. For methods other than Lachin and Foulkes, these fixed-rate +duration solves use the selected method for the fixed-design event +calculation. The input to \code{gsSurv} is a combination of the input to \code{nSurv()} and \code{gsDesign()}. +When \code{T = NULL} and \code{minfup} is specified, \code{gsSurv()} +preserves the input accrual rate and minimum follow-up, applies the group +sequential design, and solves the accrual duration needed for the final +planned number of events. +When both \code{T} and \code{minfup} are \code{NULL}, \code{gsSurv()} +preserves the input accrual rate and duration, applies the group sequential +design, and solves the follow-up duration needed for the final planned +number of events. \code{nEventsIA()} is provided to compute the expected number of events at a given point in time given enrollment, event and censoring rates. The routine diff --git a/tests/testthat/test-nSurv.R b/tests/testthat/test-nSurv.R index 8af561d5..0229df9c 100644 --- a/tests/testthat/test-nSurv.R +++ b/tests/testthat/test-nSurv.R @@ -146,6 +146,114 @@ testthat::test_that("nSurv handles T/minfup NULL for Schoenfeld and Freedman", { ) testthat::expect_s3_class(fr_t_null, "nSurv") testthat::expect_true(is.numeric(fr_t_null$T)) + testthat::expect_false(isTRUE(all.equal(sch_t_null$T, fr_t_null$T))) +}) + +testthat::test_that("non-Lachin-Foulkes methods cover allowed T/minfup combinations", { + methods <- c("Schoenfeld", "Freedman", "BernsteinLagakos") + timing_cases <- list( + fixed = list(T = 36, minfup = 12, variable = "Accrual rate"), + t_null = list(T = NULL, minfup = 12, variable = "Accrual duration"), + minfup_null = list(T = 36, minfup = NULL, variable = "Follow-up duration"), + both_null = list(T = NULL, minfup = NULL, variable = "Follow-up duration") + ) + + for (case in timing_cases) { + ns <- lapply( + methods, + function(method) { + nSurv( + lambdaC = log(2) / 6, hr = 0.5, eta = 0.001, + gamma = 4, R = 25, T = case$T, minfup = case$minfup, + ratio = 1, alpha = 0.025, beta = 0.1, method = method + ) + } + ) + for (i in seq_along(methods)) { + testthat::expect_s3_class(ns[[i]], "nSurv") + testthat::expect_identical(ns[[i]]$method, methods[i]) + testthat::expect_identical(ns[[i]]$variable, case$variable) + testthat::expect_true(all(is.finite(c(ns[[i]]$T, ns[[i]]$minfup, ns[[i]]$n, ns[[i]]$d)))) + } + ns_metric <- vapply( + ns, + function(x) { + switch(case$variable, + "Accrual rate" = x$d, + "Accrual duration" = sum(x$R), + "Follow-up duration" = x$minfup + ) + }, + numeric(1) + ) + testthat::expect_equal(length(unique(round(ns_metric, 4))), length(methods)) + + gs <- lapply( + methods, + function(method) { + gsSurv( + k = 3, test.type = 2, alpha = 0.025, beta = 0.1, + sfu = sfLDOF, timing = c(0.5, 0.75, 1), + lambdaC = log(2) / 6, hr = 0.5, eta = 0.001, + gamma = 4, R = 25, T = case$T, minfup = case$minfup, + ratio = 1, method = method + ) + } + ) + for (i in seq_along(methods)) { + testthat::expect_s3_class(gs[[i]], "gsSurv") + testthat::expect_identical(gs[[i]]$method, methods[i]) + testthat::expect_identical(gs[[i]]$variable, case$variable) + testthat::expect_true(all(is.finite(c( + gs[[i]]$T[gs[[i]]$k], + gs[[i]]$minfup, + sum(gs[[i]]$R), + gs[[i]]$n.I[gs[[i]]$k] + )))) + } + gs_events <- vapply(gs, function(x) x$n.I[x$k], numeric(1)) + testthat::expect_equal(length(unique(round(gs_events, 4))), length(methods)) + } +}) + +testthat::test_that("gsSurv solves follow-up duration with fixed accrual", { + des <- gsSurv( + k = 4, test.type = 2, alpha = 0.025, beta = 0.1, + sfu = sfLDOF, timing = c(0.25, 0.5, 0.75, 1), + lambdaC = 0.03466, hr = 0.5, eta = 0, + gamma = 15, R = 18, T = NULL, minfup = NULL, + ratio = 1, method = "Schoenfeld" + ) + + testthat::expect_equal(des$T[des$k], 25.13323, tolerance = 1e-4) + testthat::expect_equal(des$minfup, 7.13323, tolerance = 1e-4) + testthat::expect_equal(des$n.I[des$k], 89.07847, tolerance = 1e-4) + testthat::expect_equal( + sum(des$eNC[des$k, ] + des$eNE[des$k, ]), + 270, + tolerance = 1e-3 + ) + testthat::expect_identical(des$variable, "Follow-up duration") +}) + +testthat::test_that("gsSurv solves accrual duration with fixed follow-up", { + des <- gsSurv( + k = 4, test.type = 2, alpha = 0.025, beta = 0.1, + sfu = sfLDOF, timing = c(0.25, 0.5, 0.75, 1), + lambdaC = 0.03466, hr = 0.5, eta = 0, + gamma = 15, R = 18, T = NULL, minfup = 7.13323, + ratio = 1, method = "Schoenfeld" + ) + + testthat::expect_equal(des$T[des$k], 25.13323, tolerance = 1e-4) + testthat::expect_equal(des$R, 18, tolerance = 1e-4) + testthat::expect_equal(des$n.I[des$k], 89.07847, tolerance = 1e-4) + testthat::expect_equal( + sum(des$eNC[des$k, ] + des$eNE[des$k, ]), + 270, + tolerance = 1e-3 + ) + testthat::expect_identical(des$variable, "Accrual duration") }) testthat::test_that("nSurv validates S is positive", { diff --git a/vignettes/SeqDesignSurvival.Rmd b/vignettes/SeqDesignSurvival.Rmd index d1c1f590..78a9e475 100644 --- a/vignettes/SeqDesignSurvival.Rmd +++ b/vignettes/SeqDesignSurvival.Rmd @@ -31,18 +31,21 @@ group sequential clinical trials with time-to-event endpoints. Consider the example described in the SAS Documentation: [Computing Sample Size for Survival Data with Uniform Accrual](https://support.sas.com/documentation/cdl/en/statug/68162/HTML/default/statug_seqdesign_examples14.htm). -The SAS code is as follows: +The first PROC SEQDESIGN call in that example does not specify `ACCTIME`; SAS +therefore derives a range of possible accrual times. The comparison below uses +the subsequent SAS call with `ACCTIME=18`, which fixes the maximum sample size +at `15 * 18 = 270` subjects and asks SAS to solve the follow-up time. ```sas proc seqdesign; - ErrorSpend: design nstages=4 method=errfuncobf; - samplesize model=twosamplesurvival( + ErrorSpend: design nstages=4 method=errfuncobf + ; + samplesize model(ceiladjdesign=include)=twosamplesurvival( nullhazard = 0.03466 hazard = 0.01733 accrual = uniform accrate = 15 - acctime = 18 - ceiling = time); + acctime = 18); run; ``` @@ -54,12 +57,46 @@ run; - 90% power (beta = 0.10) - Equally spaced information fractions: 0.25, 0.50, 0.75, 1.00 - Schoenfeld formula for required number of events -- Sample size formula: $N = \text{ACCRATE} \times \text{ACCTIME} = 15 \times 18 = 270$ (fixed) -- Study duration $T$ solved to achieve required events (CEILING=TIME) +- Fixed accrual rate and duration, so the maximum sample size is + $N = \text{ACCRATE} \times \text{ACCTIME} = 15 \times 18 = 270$ +- Study duration $T$ solved to achieve the required events - Hazard ratio $HR = 0.01733 / 0.03466 = 0.5$ +SAS does produce a sample size in this example. In the "Sample Size Summary" +table for the `ACCTIME=18` case, it reports `Max Sample Size = 270`, +`Expected Sample Size (Null Ref) = 269.9206`, +`Expected Sample Size (Alt Ref) = 263.1141`, +`Follow-up Time = 7.133226`, and `Total Time = 25.13323`. The maximum sample +size is not calculated from the event formula; it is implied directly by the +fixed accrual rate and accrual duration: +`15 subjects/time unit * 18 time units = 270 subjects`. + +SAS also reports two sets of analysis quantities: + +- the original fractional-time design, where the analysis times are not rounded; +- the `CEILING=TIME` adjusted design, where analysis times are rounded up to + integers. + +```{r sas-reported-values} +sas_fractional <- data.frame( + Analysis = 1:4, + Events = c(22.26962, 44.53924, 66.80886, 89.07847), + Calendar_time = c(11.2631, 16.2875, 20.4926, 25.13323), + N = c(168.95, 244.31, 270.00, 270.00), + Upper_Z = c(4.33263, 2.96333, 2.35902, 2.01409) +) + +sas_ceiling_time <- data.frame( + Analysis = 1:4, + Events = c(25.11225, 48.22068, 69.38712, 92.92776), + Calendar_time = c(12, 17, 21, 26), + N = c(180, 255, 270, 270), + Upper_Z = c(4.15591, 2.90189, 2.36973, 2.01362) +) +``` + Based on these assumptions, a user naively attempting to replicate this in R -with gsDesign may attempt code like the following: +with gsDesign might pass the two-sided SAS alpha directly to `gsSurv()`: ```{r naive-r-attempt} des <- gsSurv( @@ -74,27 +111,29 @@ des <- gsSurv( eta = 0, # No dropout gamma = 15, # Uniform accrual rate of 15/month R = 18, # Accrual duration (months) - T = 25, - ratio = 1 # 1:1 randomization + T = NULL, + minfup = NULL, + ratio = 1, # 1:1 randomization + method = "Schoenfeld" ) des ``` Note that the output differs in multiple ways, including that the gsDesign -output indicates that fewer events will be required to achieve the desired -level of power compared to the SAS output. Such differences do not cast doubt -on the fidelity of either software, but rather highlight the underlying -differences in assumptions and defaults settings in the design of each -software tool. +output indicates that fewer events will be required than in the SAS output. +This is not a disagreement in the boundary calculation. It is an alpha +convention issue: `alpha = 0.05` in `gsDesign()` means 0.05 in each tail for a +symmetric two-sided design, whereas SAS's `alpha = 0.05` is the total two-sided +Type I error, or 0.025 per tail. In other words, the naive call spends roughly +twice as much Type I error as intended for the SAS comparison, leading to +less stringent boundaries and fewer required events. ### Side-by-side comparison table ```{r comparison-table} -events_sas <- c(22.27, 44.54, 66.81, 89.08) events_a2 <- round(des$n.I, 2) -z_sas <- c(4.333, 2.963, 2.359, 2.014) z_a2 <- round(des$upper$bound, 4) N2a <- sum(des$eNC[4, ]) + sum(des$eNE[4, ]) @@ -102,9 +141,9 @@ N2a <- sum(des$eNC[4, ]) + sum(des$eNE[4, ]) knitr::kable( data.frame( Analysis = 1:4, - Events_SAS = events_sas, + Events_SAS = round(sas_fractional$Events, 2), Events_gsDesign = events_a2, - Z_SAS = z_sas, + Z_SAS = round(sas_fractional$Upper_Z, 4), Z_gsDesign = z_a2 ), caption = "Naive side-by-side comparison of events and Z-boundaries at each look." @@ -120,55 +159,132 @@ functionality in gsDesign should consult `vignette("SurvivalOverview")`. ## Key differences: SAS SEQDESIGN vs. R gsDesign -There are three fundamental differences that affect the output from the example above. +There are four practical translation points that affect the output from the +example above. -### 1. Events formula +### 1. Event formula - **SAS:** @Schoenfeld1981. Uses only the null-hypothesis variance. - **gsDesign:** @LachinFoulkes by default. Uses both null and alternative hypothesis variances. L-F is slightly more conservative, giving \~1-2% more events than Schoenfeld for the same parameters. - -### 2. Alpha handling in `gsSurv()` - -O'Brien-Fleming spending makes things a bit ambiguous due to the one-sided -versus two-sided spending. - -- **SAS:** Uses a symmetric two-sided test with alpha = 0.05. - Per-tail, $z_{\alpha/2} = z_{0.025} = 1.96$. - Boundaries and event calculations both use 0.025 per tail. -- **gsSurv():** The `test.type = 2` with `alpha = 0.05` creates the same - symmetric boundaries, but computes a different group sequential design - inflation factor because it counts power from both tails. - SAS computes the inflation factor using one-sided power only. - -A possible strategy for aligning `gsSurv()` and SAS calculations in this case -is to use `test.type = 1` with `alpha = 0.025` to match SAS per-tail logic, -as we will see momentarily. - -### 3. What varies to achieve power - -- **SAS:** Fixes the accrual rate and accrual time, solves for study duration. -- **gsSurv():** Fixes study duration (`T`) and varies the accrual rate (`gamma`) - and accrual time (`R`). + Use `method = "Schoenfeld"` to match the SAS event formula. + +### 2. Alpha handling in `gsDesign()` and `gsSurv()` + +`gsDesign()` stores and spends the **one-sided** Type I error. Thus, for a +symmetric two-sided design (`test.type = 2`), `alpha = 0.025` means 0.025 in +each tail, or 0.05 total two-sided Type I error. + +This convention is deliberate: most group sequential computations in +`gsDesign()` are organized around the upper-bound crossing probability. A +symmetric two-sided design is represented by mirroring that upper-bound +spending in the lower tail. Thus the `alpha` argument is still the per-tail +crossing probability, even though the design has both an upper and lower +efficacy boundary. + +`gsSurv()` follows the same convention internally. If a user prefers to enter +the total two-sided alpha, `alpha = 0.05, sided = 2` is equivalent to +`alpha = 0.025, sided = 1` for this example because `gsSurv()` passes +`alpha / sided` to `gsDesign()`. + +Here we use `test.type = 2` and `alpha = 0.025` to make the symmetric +two-sided `gsDesign()` object match the SAS two-sided total alpha of 0.05. + +### 3. Accrual duration and follow-up time + +With `ACCTIME=18`, SAS fixes the accrual duration and total maximum sample size +and solves for the additional follow-up time. To match this in `gsSurv()`, set +both `T = NULL` and `minfup = NULL`. This tells `gsSurv()` to keep the input +accrual rate and accrual duration fixed, then solve the follow-up duration +needed for the final group sequential event requirement. + +This is a common source of apparent disagreement. For a fixed-duration +survival design, `T` is the total study duration and `minfup` is the minimum +follow-up after enrollment closes. With scalar accrual, specifying both `T` +and `minfup` makes the implied accrual duration `T - minfup`; specifying only +one of them can therefore change which quantity `gsSurv()` solves for. The SAS +comparison fixes accrual at 18 time units and lets the total time be derived. + +### 4. Fractional time vs. ceiling time + +The original SAS fractional-time design has equal information fractions and +final events 89.07847. The SAS ceiling-time adjusted design rounds analysis +times to 12, 17, 21, and 26. These rounded calendar times imply unequal +information fractions and final events 92.92776. Both are valid SAS outputs; +they should not be mixed when comparing against gsDesign. + +The distinction matters because SAS's fractional-time table answers "what +analysis times give the target information fractions?", while the ceiling-time +table answers "what happens after those analysis times are rounded up to whole +time units?" Once calendar times are rounded up, subjects are followed longer, +more events are expected, and the O'Brien-Fleming spending times are no longer +exactly 0.25, 0.50, 0.75, and 1.00. + +The translation used below is summarized as follows: + +```{r translation-table, echo=FALSE} +knitr::kable( + data.frame( + Quantity = c( + "Two-sided Type I error", + "Symmetric two-sided design", + "Event formula", + "Accrual duration", + "Total study duration", + "Follow-up after accrual" + ), + SAS = c( + "alpha = 0.05 total", + "Early stop to reject either side", + "Schoenfeld log-rank information", + "ACCTIME = 18", + "Total Time = 25.13323", + "Derived as 7.133226" + ), + gsDesign = c( + "alpha = 0.025 per tail", + "test.type = 2", + "method = \"Schoenfeld\"", + "R = 18", + "T = NULL", + "minfup = NULL" + ), + Reason = c( + "gsDesign stores and spends one-sided alpha", + "Mirrors the upper and lower efficacy boundaries", + "Avoids Lachin-Foulkes default event calculation", + "Keeps the same fixed accrual duration", + "Lets gsSurv() solve total time from fixed accrual", + "Lets gsSurv() solve the follow-up duration" + ), + check.names = FALSE + ), + caption = "Translation from the SAS PROC SEQDESIGN example to gsDesign inputs." +) +``` ## Aligning the two approaches ### `gsSurv()` with aligned parameters -Let's start our work with gsDesign by defining parameters to match SAS Example 109.14: +Let's start our work with gsDesign by defining parameters to match the SAS +fractional-time design: ```{r commonparm, message=FALSE} k <- 4 -alpha <- 0.05 # Two-sided total alpha (SAS convention) +alpha_sas <- 0.05 # Two-sided total alpha (SAS convention) +alpha_gsdesign <- alpha_sas / 2 # gsDesign uses one-sided alpha beta <- 0.10 # 1 - power = 0.10 -> 90% power lambdaC <- 0.03466 # Control hazard rate lambdaE <- 0.01733 # Experimental hazard rate HR <- lambdaE / lambdaC # = 0.5 timing <- c(0.25, 0.50, 0.75, 1.00) # Equally spaced information fractions accrate <- 15 # Uniform accrual rate (subjects per time unit) -R <- 18 # Accrual duration (time units) -N <- accrate * R +accrual_duration <- 18 # Accrual duration (time units) +sas_total_time <- 25.13323 +sas_followup_time <- sas_total_time - accrual_duration +N <- accrate * accrual_duration ``` Instead of starting with a call to `gsDesign::gsDesign()`, we begin with @@ -176,31 +292,32 @@ Instead of starting with a call to `gsDesign::gsDesign()`, we begin with `gsDesign()` (group sequential boundaries) in one call. It is the standard gsDesign function for designing time-to-event trials. -Two key differences from SAS remain even with the alpha parameter aligned with SAS: +The call below uses the same two-sided symmetric structure as SAS: -- `gsSurv()` defaults to Lachin-Foulkes sample size; Lachin-Foulkes gives - slightly more events than Schoenfeld. -- `gsSurv()` with $T$ specified **varies** the accrual rate to achieve power, - rather than fixing $N$ and solving for $T$ as SAS does. - To address this, we set the `T` parameter equal to the study duration time - calculated by SAS. +- `test.type = 2` for symmetric two-sided boundaries; +- `alpha = 0.025`, the one-sided alpha corresponding to SAS's total + two-sided alpha of 0.05; +- `method = "Schoenfeld"` for the SAS event formula; +- `T = NULL` and `minfup = NULL` so the accrual rate and accrual duration + remain fixed while `gsSurv()` solves the follow-up duration. ```{r approach2} des_2 <- gsSurv( k = 4, - test.type = 1, # One-sided (matches SAS per-tail) - alpha = 0.025, # alpha/2 per tail + test.type = 2, # Symmetric two-sided design + alpha = alpha_gsdesign, # One-sided alpha; SAS total alpha is 2 * this beta = 0.10, sfu = sfLDOF, timing = c(.25, .50, .75, 1), lambdaC = 0.03466, hr = 0.5, eta = 0, # Assume no dropout - gamma = 15, # Input rate; gsSurv will **adjust** this - R = 18, - T = 25.13323, # This is the study duration computed by SAS + gamma = accrate, + R = accrual_duration, + T = NULL, + minfup = NULL, ratio = 1, - method = "Schoenfeld" # Use Schoenfeld bounds to match SAS + method = "Schoenfeld" ) des_2 gsBoundSummary(des_2) @@ -208,36 +325,149 @@ gsBoundSummary(des_2) **Observations:** -Using `gsSurv()` with parameters that align with SAS settings, we notice that +Using `gsSurv()` with parameters that align with SAS settings, the +fractional-time output agrees with SAS. The final follow-up duration is +`r round(des_2$minfup, 5)`, giving total study duration +`r round(des_2$T[des_2$k], 5)`. + +```{r fractional-time-match} +gs_fractional <- data.frame( + Analysis = 1:k, + Events_SAS = sas_fractional$Events, + Events_gsDesign = des_2$n.I, + Time_SAS = sas_fractional$Calendar_time, + Time_gsDesign = des_2$T, + N_SAS = sas_fractional$N, + N_gsDesign = rowSums(des_2$eNC) + rowSums(des_2$eNE), + Z_SAS = sas_fractional$Upper_Z, + Z_gsDesign = des_2$upper$bound +) -- $Z$-boundaries: 4.333, 2.963, 2.359, 2.014 -> matches SAS -- Events: 23, 45, 67, 90 -> matches SAS; gsDesign is just taking the `ceiling()` - of the SAS output. -- Total N: 280 (slightly more than SAS's fixed 270). This is a result of the - fact that `gsSurv()` is varying `R` and `gamma` (corresponding conceptually - to the parameters `acctime` and `accrate` in PROC SEQDESIGN) in order to - maintain power. +knitr::kable( + round(gs_fractional, 5), + caption = "Fractional-time SAS output compared with gsSurv()." +) +``` -### Bringing it full circle with SAS PROC SEQDESIGN +The final event count is 89.07847 in both systems. The `print()` and +`gsBoundSummary()` methods display rounded integer sample sizes and events, +but the object retains the fractional values shown above. -Observe the final values for `R` and `gamma` calculated by `gsSurv()` in the -"Key inputs" table output (see above) by `des_2`: `gamma = 14.568`, and -`R = 19.133` (slightly different than our input 15 and 18). -If we substitute in these values for the `accrate` and `acctime` parameters -in SAS, we will see that the SAS calculations produce a final sample size of -`N = 278.7`, aligning with the `gsSurv()` sample size up. +If the SAS follow-up time is already known, the equivalent fixed-follow-up +translation is `T = NULL` with `minfup` set to the SAS follow-up time. In that +case, `gsSurv()` holds the follow-up duration fixed and solves the accrual +duration needed for the final group sequential event target: -``` sas -proc seqdesign; - ErrorSpend: design nstages=4 method=errfuncobf; - samplesize model=twosamplesurvival( - nullhazard = 0.03466 - hazard = 0.01733 - accrual = uniform - accrate = 14.568 - acctime = 19.133 - ceiling = time); -run; +```{r fixed-followup-check} +des_fixed_followup <- gsSurv( + k = 4, + test.type = 2, + alpha = alpha_gsdesign, + beta = 0.10, + sfu = sfLDOF, + timing = c(.25, .50, .75, 1), + lambdaC = 0.03466, + hr = 0.5, + eta = 0, + gamma = accrate, + R = accrual_duration, + T = NULL, + minfup = sas_followup_time, + ratio = 1, + method = "Schoenfeld" +) + +fixed_followup_check <- data.frame( + Quantity = c( + "Final study duration", + "Accrual duration", + "Final events", + "Final N" + ), + Solved_followup = c( + des_2$T[k], + sum(des_2$R), + des_2$n.I[k], + sum(des_2$eNC[k, ] + des_2$eNE[k, ]) + ), + Specified_followup = c( + des_fixed_followup$T[k], + sum(des_fixed_followup$R), + des_fixed_followup$n.I[k], + sum(des_fixed_followup$eNC[k, ] + des_fixed_followup$eNE[k, ]) + ) +) +fixed_followup_check[-1] <- lapply(fixed_followup_check[-1], round, digits = 5) + +knitr::kable( + fixed_followup_check, + caption = "Both fixed-accrual translations produce the same final design." +) ``` +### Matching the SAS ceiling-time adjusted design + +The SAS example also reports a `CEILING=TIME` adjusted design. This is not the +same design as the equal-information fractional-time output above. The analysis +times are rounded up to 12, 17, 21, and 26, which increases the expected events +and changes the information fractions. + +We can reproduce the ceiling-time event counts directly from the fixed accrual +rate, fixed accrual duration, and exponential event rates. With equal +randomization, the control and experimental accrual rates are each 7.5 per time +unit. + +```{r ceiling-time-match} +event_count_at_time <- function(time) { + control_events <- eEvents( + lambda = lambdaC, + gamma = accrate / (1 + 1), + R = accrual_duration, + T = time + )$d + experimental_events <- eEvents( + lambda = lambdaE, + gamma = accrate / (1 + 1), + R = accrual_duration, + T = time + )$d + sum(control_events) + sum(experimental_events) +} + +ceiling_times <- ceiling(sas_fractional$Calendar_time) +ceiling_events <- vapply(ceiling_times, event_count_at_time, numeric(1)) + +des_ceiling <- gsDesign( + k = k, + test.type = 2, + alpha = alpha_gsdesign, + beta = beta, + sfu = sfLDOF, + n.I = ceiling_events, + timing = ceiling_events / max(ceiling_events) +) + +gs_ceiling <- data.frame( + Analysis = 1:k, + Events_SAS = sas_ceiling_time$Events, + Events_gsDesign = ceiling_events, + Time_SAS = sas_ceiling_time$Calendar_time, + Time_gsDesign = ceiling_times, + N_SAS = sas_ceiling_time$N, + N_gsDesign = pmin(accrate * ceiling_times, N), + Z_SAS = sas_ceiling_time$Upper_Z, + Z_gsDesign = des_ceiling$upper$bound +) + +knitr::kable( + round(gs_ceiling, 5), + caption = "Ceiling-time SAS output compared with gsDesign()." +) +``` + +The remaining small differences in the printed Z-values are numerical rounding +differences. The important conceptual distinction is that the SAS +fractional-time and ceiling-time tables are different designs: one uses equal +information fractions, and the other uses rounded calendar analysis times. + ## References From 9e778930aaaccadca13a5af72713f0d233160f8d Mon Sep 17 00:00:00 2001 From: keaven Date: Tue, 26 May 2026 07:09:12 -0400 Subject: [PATCH 11/23] Document SAS survival alpha convention --- DESCRIPTION | 2 +- NEWS.md | 8 +++++++ vignettes/SeqDesignSurvival.Rmd | 39 ++++++++++++++------------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index a6970d8a..c96d6bdb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: gsDesign -Version: 3.10.0 +Version: 3.10.0.9000 Title: Group Sequential Design Authors@R: c( person("Keaven", "Anderson", email = "keaven_anderson@merck.com", role = c("aut", "cre")), diff --git a/NEWS.md b/NEWS.md index d8868052..55c286e2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,11 @@ +# gsDesign (development version) + +## Documentation + +- Updated the `SeqDesignSurvival` vignette to use the one-sided `gsSurv()` + alpha convention when reproducing SAS PROC SEQDESIGN fractional-time + survival output (#264). + # gsDesign 3.10.0 (May 2026) ## New features diff --git a/vignettes/SeqDesignSurvival.Rmd b/vignettes/SeqDesignSurvival.Rmd index 78a9e475..02a3cfc7 100644 --- a/vignettes/SeqDesignSurvival.Rmd +++ b/vignettes/SeqDesignSurvival.Rmd @@ -71,11 +71,9 @@ size is not calculated from the event formula; it is implied directly by the fixed accrual rate and accrual duration: `15 subjects/time unit * 18 time units = 270 subjects`. -SAS also reports two sets of analysis quantities: - -- the original fractional-time design, where the analysis times are not rounded; -- the `CEILING=TIME` adjusted design, where analysis times are rounded up to - integers. +SAS reports two sets of analysis quantities. We focus first on the +fractional-time design, where the analysis times are not rounded, and then +return to the `CEILING=TIME` adjusted design later. ```{r sas-reported-values} sas_fractional <- data.frame( @@ -95,14 +93,14 @@ sas_ceiling_time <- data.frame( ) ``` -Based on these assumptions, a user naively attempting to replicate this in R -with gsDesign might pass the two-sided SAS alpha directly to `gsSurv()`: +gsDesign uses one-sided alpha by default, so we need to convert the two-sided SAS alpha to a one-sided alpha. +The two-sided alpha is 0.05, so the one-sided alpha is 0.025. -```{r naive-r-attempt} +```{r aligned-r-attempt} des <- gsSurv( k = 4, test.type = 2, - alpha = 0.05, + alpha = 0.025, beta = 0.10, sfu = sfLDOF, # Lan-DeMets O'Brien--Fleming timing = c(.25, .50, .75, 1), # Equal information fractions @@ -117,17 +115,14 @@ des <- gsSurv( method = "Schoenfeld" ) -des +des |> gsBoundSummary(tdigits = 2, ddigits = 2) |> gt::gt() ``` -Note that the output differs in multiple ways, including that the gsDesign -output indicates that fewer events will be required than in the SAS output. -This is not a disagreement in the boundary calculation. It is an alpha -convention issue: `alpha = 0.05` in `gsDesign()` means 0.05 in each tail for a -symmetric two-sided design, whereas SAS's `alpha = 0.05` is the total two-sided -Type I error, or 0.025 per tail. In other words, the naive call spends roughly -twice as much Type I error as intended for the SAS comparison, leading to -less stringent boundaries and fewer required events. +This call now matches the SAS fractional-time design. The key inputs are +`alpha = 0.025`, which represents the one-sided alpha corresponding to SAS's +two-sided total alpha of 0.05; `method = "Schoenfeld"`, which matches the SAS +event formula; and `T = NULL, minfup = NULL`, which fixes the accrual duration +and solves the follow-up time. ### Side-by-side comparison table @@ -146,14 +141,12 @@ knitr::kable( Z_SAS = round(sas_fractional$Upper_Z, 4), Z_gsDesign = z_a2 ), - caption = "Naive side-by-side comparison of events and Z-boundaries at each look." + caption = "Side-by-side comparison of events and Z-boundaries at each look." ) ``` -The rest of this vignette works to resolve the apparent discrepancies in -results, using the above example as a reference case. We identify the key -assumptions in each system, explain why naive parameter translation produces -different results, and show how to obtain matching output. +The rest of this vignette identifies the key assumptions in each system and +explains why alternative parameter translations can produce different results. Those who would like to know more details about the design of the time-to-event functionality in gsDesign should consult `vignette("SurvivalOverview")`. From 51aac8fb2e83b73921dbe1d6126e3046aca9b8df Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:15:12 -0700 Subject: [PATCH 12/23] Remove duplicated AI slop files --- .Rbuildignore | 8 -- .gemini/styleguide.md | 22 ----- .github/copilot-instructions.md | 63 ------------- CLAUDE.md | 63 ------------- GEMINI.md | 63 ------------- _pkgdown.yml | 1 - inst/ai/AGENTS.md | 63 ------------- inst/ai/CLAUDE.md | 63 ------------- inst/ai/GEMINI.md | 63 ------------- inst/ai/README.md | 28 ------ inst/ai/copilot-instructions.md | 63 ------------- inst/ai/gemini-styleguide.md | 22 ----- vignettes/gsDesignAISkills.Rmd | 159 -------------------------------- 13 files changed, 681 deletions(-) delete mode 100644 .gemini/styleguide.md delete mode 100644 .github/copilot-instructions.md delete mode 100644 CLAUDE.md delete mode 100644 GEMINI.md delete mode 100644 inst/ai/AGENTS.md delete mode 100644 inst/ai/CLAUDE.md delete mode 100644 inst/ai/GEMINI.md delete mode 100644 inst/ai/README.md delete mode 100644 inst/ai/copilot-instructions.md delete mode 100644 inst/ai/gemini-styleguide.md delete mode 100644 vignettes/gsDesignAISkills.Rmd diff --git a/.Rbuildignore b/.Rbuildignore index ea65bb2d..4fb5fb33 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,15 +4,7 @@ ^_pkgdown\.yml$ ^pkgdown$ ^\.github$ -^\.gemini$ -^\.cursor$ -^\.local-r-lib$ ^AGENTS\.md$ -^CLAUDE\.md$ -^GEMINI\.md$ -^doc$ -^docs$ -^Meta$ ^codecov\.yml$ ^LICENSE\.md$ ^cran-comments\.md$ diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md deleted file mode 100644 index 6460e4dc..00000000 --- a/.gemini/styleguide.md +++ /dev/null @@ -1,22 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when reviewing or modifying the `keaven/gsDesign` R package. - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. -- Prefer `rg` for code searches. -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`; for example, "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` for event-driven or information-fraction timing. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but use `simBinomialSeasonalExact()`/`toBinomialExact()` only when the endpoint/workflow is seasonal rare-event exact-binomial monitoring. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. -- Include issue numbers when known, for example `(#264)`. -- Run focused `testthat::test_file()` checks for touched areas before broader tests. -- Before release-style commits, run `pkgload::load_all(".")` and `testthat::test_dir("tests/testthat")`. -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`; restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- Rebuild local pkgdown with `pkgdown::build_site()` when Rd, vignettes, NEWS, or local docs are updated. -- pkgdown writes to ignored `docs/`; generated site files normally do not appear in `git status`. -- Stage only intentional source, documentation, tests, NEWS, and version changes. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/GEMINI.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/_pkgdown.yml b/_pkgdown.yml index 04891a05..c8bd5b06 100755 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -143,7 +143,6 @@ articles: contents: - GentleIntroductionToGSD - gsDesignPackageOverview - - gsDesignAISkills - SpendingFunctionOverview - nNormal - binomialTwoSample diff --git a/inst/ai/AGENTS.md b/inst/ai/AGENTS.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/inst/ai/AGENTS.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/CLAUDE.md b/inst/ai/CLAUDE.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/inst/ai/CLAUDE.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/GEMINI.md b/inst/ai/GEMINI.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/inst/ai/GEMINI.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/README.md b/inst/ai/README.md deleted file mode 100644 index 0c846614..00000000 --- a/inst/ai/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# gsDesign AI Workflow Instructions - -This directory contains coding-agent instruction files for working on -`gsDesign` development tasks. - -The installed package path can be found with: - -```r -system.file("ai", package = "gsDesign") -``` - -Suggested uses: - -- Copy `AGENTS.md` to a repository root for Codex-style coding agents. -- Copy `CLAUDE.md` to a repository root for Claude Code. -- Copy `GEMINI.md` to a repository root for Gemini CLI. -- Copy `copilot-instructions.md` to `.github/copilot-instructions.md`. -- Copy `gemini-styleguide.md` to `.gemini/styleguide.md`. - -These files are not full statistical methods documentation. They are workflow -prompts for coding agents so routine package-development tasks use consistent -testing, NEWS, version, pkgdown, commit, and push practices. They also include -brief function-selection guidance, such as using `gsSurvCalendar()` when a -request specifies analyses at calendar months after enrollment opens, while -preserving the original design specifications unless the user asks to change -them. Very low planned event counts can cue a discussion of exact-binomial -rare-event methods, but should not by itself override a calendar-timed survival -design request. diff --git a/inst/ai/copilot-instructions.md b/inst/ai/copilot-instructions.md deleted file mode 100644 index 55ed1d7c..00000000 --- a/inst/ai/copilot-instructions.md +++ /dev/null @@ -1,63 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when working in the `keaven/gsDesign` R package. - -## Repository Workflow - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. -- Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. - -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS - -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version. - Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. -- Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. -- Include the issue number when known, for example `(#264)`. - -## Testing - -- Run focused `testthat::test_file()` checks for touched areas before broad tests. -- Before release-style commits, run: - -```r -pkgload::load_all(".") -testthat::test_dir("tests/testthat") -``` - -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- When changing `DESCRIPTION`, verify: - -```r -pkgload::load_all(".") -as.character(utils::packageVersion("gsDesign")) -``` - -## pkgdown - -- Rebuild local pkgdown with: - -```r -pkgdown::build_site() -``` - -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. - -## Commit And Push - -- Stage only intentional source, documentation, tests, NEWS, and version changes. -- Use concise issue-focused commit messages. -- Push the current branch explicitly with `git push origin `. diff --git a/inst/ai/gemini-styleguide.md b/inst/ai/gemini-styleguide.md deleted file mode 100644 index 6460e4dc..00000000 --- a/inst/ai/gemini-styleguide.md +++ /dev/null @@ -1,22 +0,0 @@ -# gsDesign Agent Instructions - -Use these instructions when reviewing or modifying the `keaven/gsDesign` R package. - -- Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. -- Prefer `rg` for code searches. -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`; for example, "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` for event-driven or information-fraction timing. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but use `simBinomialSeasonalExact()`/`toBinomialExact()` only when the endpoint/workflow is seasonal rare-event exact-binomial monitoring. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. -- For development work after a release, bump `DESCRIPTION` to the next `.900x` version, such as `3.9.1.9000`. -- Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`, under the existing appropriate heading. -- Include issue numbers when known, for example `(#264)`. -- Run focused `testthat::test_file()` checks for touched areas before broader tests. -- Before release-style commits, run `pkgload::load_all(".")` and `testthat::test_dir("tests/testthat")`. -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`; restore unintended snapshot deletions before committing. -- Run `git diff --check` before staging or committing. -- Rebuild local pkgdown with `pkgdown::build_site()` when Rd, vignettes, NEWS, or local docs are updated. -- pkgdown writes to ignored `docs/`; generated site files normally do not appear in `git status`. -- Stage only intentional source, documentation, tests, NEWS, and version changes. diff --git a/vignettes/gsDesignAISkills.Rmd b/vignettes/gsDesignAISkills.Rmd deleted file mode 100644 index 8d571882..00000000 --- a/vignettes/gsDesignAISkills.Rmd +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: "Using gsDesign AI skills" -output: rmarkdown::html_vignette -vignette: > - %\VignetteIndexEntry{Using gsDesign AI skills} - %\VignetteEngine{knitr::rmarkdown} - %\VignetteEncoding{UTF-8} ---- - -```{r, include=FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -## Overview - -The `gsDesign` repository includes lightweight AI workflow instructions for -coding agents. These files are not full statistical methods documentation. They -are promptable development guides that help coding agents use consistent package -maintenance habits: checking git status, updating `NEWS.md`, bumping development -versions, running focused and broad tests, rebuilding pkgdown, and pushing -issue branches. They also include brief function-selection cues for common -design requests, such as using `gsSurvCalendar()` when the prompt fixes analyses -at calendar months after enrollment opens, while preserving the original design -specifications unless the user asks to change them. Very low planned event -counts can cue a discussion of exact-binomial rare-event methods, but should not -by itself override a calendar-timed survival design request. - -The repository includes native instruction files for common coding agents: - -- `AGENTS.md` for Codex-style agents. -- `CLAUDE.md` for Claude Code. -- `GEMINI.md` for Gemini CLI. -- `.github/copilot-instructions.md` for GitHub Copilot. -- `.gemini/styleguide.md` for Gemini Code Assist. - -The installed R package also ships copies under `inst/ai/`, available with: - -```{r, eval=FALSE} -system.file("ai", package = "gsDesign") -``` - -This makes the instructions available to users who install the package without -cloning the GitHub repository. - -## Installing instructions in a project - -After installing `gsDesign`, you can copy an instruction file into a local -project. For example, to add Codex-style instructions: - -```{r, eval=FALSE} -file.copy( - from = system.file("ai", "AGENTS.md", package = "gsDesign"), - to = "AGENTS.md", - overwrite = FALSE -) -``` - -For GitHub Copilot: - -```{r, eval=FALSE} -dir.create(".github", showWarnings = FALSE) -file.copy( - from = system.file("ai", "copilot-instructions.md", package = "gsDesign"), - to = file.path(".github", "copilot-instructions.md"), - overwrite = FALSE -) -``` - -For Gemini Code Assist: - -```{r, eval=FALSE} -dir.create(".gemini", showWarnings = FALSE) -file.copy( - from = system.file("ai", "gemini-styleguide.md", package = "gsDesign"), - to = file.path(".gemini", "styleguide.md"), - overwrite = FALSE -) -``` - -## Example prompt and response - -An AI skill works best when the user gives a specific package-development or -design-coding task and lets the instruction file supply the package workflow -and routing details. - -Example prompt: - -> Use the gsDesign AI instructions. I am designing a standard time-to-event -> group sequential trial, not a seasonal rare-event exact-binomial example. -> Start with the original design specs: non-binding futility (`test.type = 4`), -> information-based spending, Hwang-Shih-DeCani efficacy spending with -> `sfupar = -4`, Hwang-Shih-DeCani futility spending with `sflpar = -2`, -> one-sided alpha 0.025, beta 0.1, a control median of 12 months, null HR 1, -> alternative HR 0.7, no dropout, uniform enrollment over 18 months, and -> minimum follow-up of 18 months. Keep those specs. Use equal randomization and -> add another interim analysis at 24 months after enrollment opens, so analyses -> occur at 12, 24, and 36 months. Show the R code and explain which gsDesign -> function matches this text. - -Example response: - -> The phrase "analyses at 12, 24, and 36 months after enrollment opens" fixes -> the analysis schedule on the calendar scale, so I would use -> `gsSurvCalendar(calendarTime = c(12, 24, 36))`. The timing request changes -> the analysis schedule only; the original test type, spending functions, -> alpha, beta, hazard assumptions, dropout, accrual, and follow-up inputs are -> preserved. I would not use `simBinomialSeasonalExact()` because the prompt -> does not describe a seasonal rare-event exact-binomial monitoring problem. -> If the resulting planned event counts are very low, such as fewer than 100 -> events, I might mention exact-binomial rare-event methods as a sensitivity or -> alternative workflow to consider, but I would not switch to them unless the -> endpoint and monitoring plan are actually binomial rare-event based. Equal -> randomization is `ratio = 1`. - -```r -library(gsDesign) - -design <- gsSurvCalendar( - calendarTime = c(12, 24, 36), - spending = "information", - test.type = 4, - alpha = 0.025, - beta = 0.10, - sfu = sfHSD, - sfupar = -4, - sfl = sfHSD, - sflpar = -2, - lambdaC = log(2) / 12, - hr = 0.70, - hr0 = 1, - eta = 0, - gamma = 1, - R = 18, - minfup = 18, - ratio = 1 -) - -design_integer <- toInteger(design) -gsBoundSummary(design_integer) -``` - -The response is useful because it maps the user's timing language to the -appropriate `gsDesign` function before writing code. It also distinguishes -calendar timing from calendar spending: `spending = "information"` keeps -spending tied to information, while `spending = "calendar"` would tie spending -to the calendar fractions. The original design specifications are kept in the -function call rather than being replaced by generic defaults. The low-count -heuristic is treated as a prompt to discuss alternatives, not as permission to -change the design family. - -## What these instructions do not do - -AI workflow instructions do not change the statistical behavior of `gsDesign`. -They also do not replace human review. They help make agent-assisted package -maintenance more reproducible by placing the expected repository workflow where -coding agents can find it. From dc03128d1e02cfaaf8bcf50396c2e9bec17744a4 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:16:16 -0700 Subject: [PATCH 13/23] Split part of AGENTS.md into survival design routing skill --- .../skills/survival-design-routing/SKILL.md | 23 ++++++++++++ AGENTS.md | 37 ++++++++++--------- 2 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 .agents/skills/survival-design-routing/SKILL.md diff --git a/.agents/skills/survival-design-routing/SKILL.md b/.agents/skills/survival-design-routing/SKILL.md new file mode 100644 index 00000000..bcf919cf --- /dev/null +++ b/.agents/skills/survival-design-routing/SKILL.md @@ -0,0 +1,23 @@ +--- +name: survival-design-routing +description: Choose the appropriate gsDesign survival or exact-binomial workflow when requests involve calendar-time interim analyses, event- or information-driven survival looks, seasonal rare-event exact-binomial monitoring, or explicit randomization ratios; covers when to use gsSurvCalendar(), gsSurv(), simBinomialSeasonalExact(), and toBinomialExact(). +--- + +# gsDesign survival design routing + +- If a request specifies analyses by calendar dates or months from trial start + or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. + Example: "add an interim analysis 24 months after enrollment opens" means + include `24` in `calendarTime`. +- When changing only analysis timing, preserve the original design + specifications unless the user asks to change them. +- Use `gsSurv()` when timing is event-driven or specified by information + fractions rather than fixed calendar times. +- Very low planned event counts, such as fewer than 100 total events, can be + a cue to discuss exact-binomial rare-event methods, but do not switch + solely because counts are low. +- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the + endpoint/workflow is seasonal rare-event exact-binomial monitoring; + otherwise keep the appropriate survival design function. +- Set `ratio` explicitly when randomization is specified; `ratio = 1` means + equal experimental:control randomization. diff --git a/AGENTS.md b/AGENTS.md index 55ed1d7c..ce731ceb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,28 +1,23 @@ -# gsDesign Agent Instructions +# AGENTS.md Use these instructions when working in the `keaven/gsDesign` R package. -## Repository Workflow +## Repository workflow - Check `git status --short --branch` before edits, staging, committing, or pulling. -- Preserve unrelated user changes. Do not revert generated or user-edited files unless asked. +- Preserve unrelated user changes. Do not revert generated or user-edited files + unless asked. - Prefer `rg` for code searches. -- When an issue branch starts with a number, infer the likely GitHub issue from that number, but verify exact issue metadata when needed. +- When an issue branch starts with a number, infer the likely GitHub issue from + that number, but verify exact issue metadata when needed. -## gsDesign Function Selection - -- If a request specifies analyses by calendar dates or months from trial start or enrollment opening, use `gsSurvCalendar(calendarTime = ...)`. - Example: "add an interim analysis 24 months after enrollment opens" means include `24` in `calendarTime`. -- When changing only analysis timing, preserve the original design specifications unless the user asks to change them. -- Use `gsSurv()` when timing is event-driven or specified by information fractions rather than fixed calendar times. -- Very low planned event counts, such as fewer than 100 total events, can be a cue to discuss exact-binomial rare-event methods, but do not switch solely because counts are low. -- Use `simBinomialSeasonalExact()` and `toBinomialExact()` when the endpoint/workflow is seasonal rare-event exact-binomial monitoring; otherwise keep the appropriate survival design function. -- Set `ratio` explicitly when randomization is specified; `ratio = 1` means equal experimental:control randomization. - -## Version And NEWS +## Version in DESCRIPTION file - For development work after a release, bump `DESCRIPTION` to the next `.900x` version. Example: `3.9.1` becomes `3.9.1.9000`; if already `.9000`, increment to `.9001`. + +## NEWS.md + - Add concise entries to the top `# gsDesign (development version)` section of `NEWS.md`. - Use the existing headings such as `## Bug fixes`, `## Documentation`, and `## Testing`. - Include the issue number when known, for example `(#264)`. @@ -37,7 +32,9 @@ pkgload::load_all(".") testthat::test_dir("tests/testthat") ``` -- Full test runs may delete RTF snapshots under `tests/testthat/_snaps/independent-test-as_rtf/`. Restore unintended snapshot deletions before committing. +- Full test runs may delete RTF snapshots under + `tests/testthat/_snaps/independent-test-as_rtf/`. + Restore unintended snapshot deletions before committing. - Run `git diff --check` before staging or committing. - When changing `DESCRIPTION`, verify: @@ -54,10 +51,14 @@ as.character(utils::packageVersion("gsDesign")) pkgdown::build_site() ``` -- pkgdown writes to `docs/`, which is ignored in this checkout. Do not expect generated site files in `git status` unless ignore rules change. +- pkgdown writes to `docs/`, which is ignored in this checkout. + Do not expect generated site files in `git status` unless ignore rules change. -## Commit And Push +## Commit and push +- Run `devtools::document()` before staging or committing changes to update + documentation and NAMESPACE. - Stage only intentional source, documentation, tests, NEWS, and version changes. - Use concise issue-focused commit messages. - Push the current branch explicitly with `git push origin `. + Do not push to `main` directly. From c24273a093e98bc7b818a472152d1aa4a57eee80 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:16:40 -0700 Subject: [PATCH 14/23] Reorder build ignore items --- .Rbuildignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Rbuildignore b/.Rbuildignore index 4fb5fb33..02f74bf8 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,10 +4,10 @@ ^_pkgdown\.yml$ ^pkgdown$ ^\.github$ -^AGENTS\.md$ ^codecov\.yml$ ^LICENSE\.md$ ^cran-comments\.md$ ^revdep$ tests/benchmarks ^\.agents$ +^AGENTS\.md$ From 30968226561986f5ba67bb848bb1da5cc298a6e1 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:16:58 -0700 Subject: [PATCH 15/23] Reset URL to the correct address --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42487868..87732779 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The gsDesign package supports group sequential clinical trial design, largely as presented in the book Group Sequential Methods with Applications to Clinical Trials by Christopher Jennison and Bruce Turnbull (Chapman and Hall/CRC, 2000). -An easy-to-use [web interface](https://rinpharma.shinyapps.io/gsdesign/) to both enable usage without coding and to generate code to be able to reproduce the design; this is being enhanced to support more features on an ongoing basis. See the [talk](https://www.youtube.com/embed/8uZRuvzma9M) presented at the R/Pharma Conference to get started with the web interface. +An easy-to-use [web interface](https://rinpharma.shinyapps.io/gsdesign/) to both enable usage without coding and to generate code to be able to reproduce the design; this is being enhanced to support more features on an ongoing basis. See the [talk](https://www.youtube.com/watch?v=8uZRuvzma9M) presented at the R/Pharma Conference to get started with the web interface. With improving large language models since late 2025, updates and additions to the gsDesign package have accelerated. ## Installation From 67eac0b2878d6ffacdc56936c3ee028efde3a8fb Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:17:15 -0700 Subject: [PATCH 16/23] Reset version number to 3.9.0.9006 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index c96d6bdb..789c64dc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: gsDesign -Version: 3.10.0.9000 +Version: 3.9.0.9006 Title: Group Sequential Design Authors@R: c( person("Keaven", "Anderson", email = "keaven_anderson@merck.com", role = c("aut", "cre")), From 7ee1f40c5d599e05d9809de65821783dbd8cfca1 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:17:21 -0700 Subject: [PATCH 17/23] Run roxygen2 --- man/toInteger.Rd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/man/toInteger.Rd b/man/toInteger.Rd index cb39ed5d..3ac2bfe6 100644 --- a/man/toInteger.Rd +++ b/man/toInteger.Rd @@ -86,10 +86,6 @@ and harm spending for \code{test.type} 7 or 8) are carried from the input design into the internal \code{gsDesign()} recomputation so skipped looks stay skipped after integer rounding. } -\seealso{ -\code{\link{gsSurv}}, \code{\link{toBinomialExact}}, - \code{vignette("MultiSeasonRareEvents", package = "gsDesign")} -} \examples{ # The following code derives the group sequential design using the method # of Lachin and Foulkes @@ -118,3 +114,7 @@ x <- gsSurv( # with final event count rounded up by default. toInteger(x) } +\seealso{ +\code{\link{gsSurv}}, \code{\link{toBinomialExact}}, + \code{vignette("MultiSeasonRareEvents", package = "gsDesign")} +} From 73113ed68b313376cf7f91ea17fbe81a8ea408a7 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:30:07 -0700 Subject: [PATCH 18/23] Clean up unnecessary commit hashes and changelog entries --- NEWS.md | 161 +++++++++++++++++++++++++------------------------------- 1 file changed, 71 insertions(+), 90 deletions(-) diff --git a/NEWS.md b/NEWS.md index 55c286e2..1d03b4c6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,20 +1,12 @@ # gsDesign (development version) -## Documentation - -- Updated the `SeqDesignSurvival` vignette to use the one-sided `gsSurv()` - alpha convention when reproducing SAS PROC SEQDESIGN fractional-time - survival output (#264). - -# gsDesign 3.10.0 (May 2026) - ## New features - Added `testUpper`, `testLower`, and `testHarm` parameters to `gsDesign()`, `gsSurv()`, and `gsSurvCalendar()` for selective bound testing at interim analyses. Each accepts a logical scalar or vector of length `k` specifying which analyses should include that boundary. Inactive bounds are set to - extreme values (±20 on Z-scale) and displayed as `NA` in `print()` and + extreme values (±20 on Z-scale) and displayed as `NA` in `print()` and `gsBoundSummary()` output. This enables designs such as futility-only at early interims, deferred efficacy testing, or selective harm monitoring (@keaven, #141). @@ -42,7 +34,7 @@ timing. Unlike `gsSurv()` and `gsSurvCalendar()` which solve for sample size, `gsSurvPower()` takes fixed assumptions and computes power. Supports calendar-time and event-driven timing, stratified designs, all test types - (1–8 including harm bounds), and flexible analysis timing criteria + (1--8 including harm bounds), and flexible analysis timing criteria (`targetEvents`, `plannedCalendarTime`, `maxExtension`, `minTimeFromPreviousAnalysis`, `minN`, `minFollowUp`). When an existing `gsSurv` design is provided via `x`, parameters can be selectively @@ -117,6 +109,9 @@ ## Documentation +- Updated the `SeqDesignSurvival` vignette to use the one-sided `gsSurv()` + alpha convention when reproducing SAS PROC SEQDESIGN fractional-time + survival output (#264). - Corrected and generalized the multi-season rare-event vignette so enrollment timing, planned counts, and simulation event-rate inputs are derived from the stated design specifications, with calendar-timed seasonal analyses, @@ -156,20 +151,6 @@ (c1065ea8, 2e9260bd). - Reorganized pkgdown article sections to separate general materials, exact binomial workflows, and multiple-hypothesis-testing content (67146132). -- Added repository-level coding agent instructions for Codex, Claude Code, - Gemini, and GitHub Copilot to support consistent gsDesign issue-branch, - testing, NEWS/version, pkgdown, commit, and push workflows (04cc7929). -- Added vignette "Using gsDesign AI skills" - (`vignette("gsDesignAISkills")`) and package-shipped AI workflow templates - under `inst/ai/` so users can copy the agent instructions into their own - projects after installing `gsDesign` (8dd96c3b). -- Expanded the AI instructions and vignette example with function-selection - guidance that maps calendar-timed survival-design prompts, such as a - 24-month interim after enrollment opens with `ratio = 1`, to - `gsSurvCalendar()` while preserving the original design specifications, and - clarifies that very low event counts can cue discussion of exact-binomial - rare-event methods without automatically changing the design family - (b1eae2f0). ## Testing @@ -314,7 +295,7 @@ - New function `binomialPowerTable()` generates power tables across control rates and treatment effects. Supports both analytical calculations and - fast simulation for exact results (c466a616). + fast simulation for exact results. # gsDesign 3.6.8 (May 2025) @@ -506,23 +487,23 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.5.0 (July 2023) -- `sfPower()` now allows a wider parameter range (0, 15] (5502e4a1). +- `sfPower()` now allows a wider parameter range (0, 15]. - `toInteger()` function added to convert `gsDesign` or `gsSurv` classes - to integer sample size and event counts (5502e4a1). + to integer sample size and event counts. - `toBinomialExact()` function added to convert time-to-event bounds to - exact binomial for low event rate studies (5502e4a1). + exact binomial for low event rate studies. - Added "A Gentle Introduction to Group Sequential Design" vignette for - an introduction to asymptotics for group sequential design (5502e4a1). + an introduction to asymptotics for group sequential design. - `as_table()` and `as_gt()` methods for `gsBinomialExact` objects added, - as described in the new "Binomial SPRT" vignette (5502e4a1). + as described in the new "Binomial SPRT" vignette. - In `plot.ssrCP()`, the `hat` syntax in the mathematical expression is revised, - resolving labeling issues (5502e4a1). + resolving labeling issues. - `ggplot2::qplot()` usage replaced due to its deprecation in ggplot2 3.4.0 - (5502e4a1). + . - Link update for the gsDesign manual in the documentation, now directly pointing to the gsDesign technical manual bookdown project - (5502e4a1). -- Introduced a new hex sticker logo (5502e4a1). + . +- Introduced a new hex sticker logo. # gsDesign 3.4.0 (October 2022) @@ -536,98 +517,98 @@ We have made the spending function summary output more readable and informative. - Addition of vignettes - Demonstrate cure model and calendar-based analysis timing for time-to-event endpoint design - Vaccine efficacy design using spending bounds and exact binomial boundary crossing probabilities - (85f1875f). -- Minor fix to labeling in print.gsProbability (85f1875f). -- Fixed error in sfStep (85f1875f). -- Updates to reduce R CMD check and other minor issues (85f1875f). + . +- Minor fix to labeling in print.gsProbability. +- Fixed error in sfStep. +- Updates to reduce R CMD check and other minor issues. # gsDesign 3.2.2 (January 2022) -- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate (cbf9a940). -- Correctly close graphics device in unit tests to avoid plot output file not found issues (cbf9a940). -- Minor fixes to hGraph() for multiplicity graphs (cbf9a940). -- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue (cbf9a940). -- Minor changes to vignettes (cbf9a940). +- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate. +- Correctly close graphics device in unit tests to avoid plot output file not found issues. +- Minor fixes to hGraph() for multiplicity graphs. +- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue. +- Minor changes to vignettes. # gsDesign 3.2.1 (July 2021) -- Changed gt package usage in a vignette due to deprecated gt function (eae74e2b). -- Replied to minor comments from CRAN reviewer (no functionality impact) (eae74e2b). -- Minor update to DESCRIPTION citing Jennison and Turnbull reference (eae74e2b). +- Changed gt package usage in a vignette due to deprecated gt function. +- Replied to minor comments from CRAN reviewer (no functionality impact). +- Minor update to DESCRIPTION citing Jennison and Turnbull reference. # gsDesign 3.2.0 (January 2021) -- Substantially updated unit testing to increase code coverage above 80% (96e49fd7). -- Updated error checking messages to print function where check fails (96e49fd7). -- Removed dependencies on plyr packages (96e49fd7). -- Updated github actions (96e49fd7). +- Substantially updated unit testing to increase code coverage above 80%. +- Updated error checking messages to print function where check fails. +- Removed dependencies on plyr packages. +- Updated github actions. # gsDesign 3.1.1 (May 2020) -- Vignettes updated (520c23e4). -- Added `hGraph()` to support ggplot2 versions of multiplicity graphs (520c23e4). -- Eliminated unnecessary check from `sequentialPValue` (520c23e4). -- Targeted release to CRAN (520c23e4). -- Removed dependencies on reshape2, plyr (520c23e4). -- Updated continuous integration (520c23e4). -- Updated license (520c23e4). +- Vignettes updated. +- Added `hGraph()` to support ggplot2 versions of multiplicity graphs. +- Eliminated unnecessary check from `sequentialPValue`. +- Targeted release to CRAN. +- Removed dependencies on reshape2, plyr. +- Updated continuous integration. +- Updated license. # gsDesign 3.1.0 (April 2019) -- Addition of pkgdown web site (b4001d4d). -- Updated unit testing to from RUnit to testthat (b4001d4d). -- Converted to roxygen2 generation of help files (b4001d4d). -- Converted vignettes to R Markdown (b4001d4d). -- Added Travis-CI and Appveyor support (b4001d4d). -- Added `sequentialPValue` function (b4001d4d). -- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv` (b4001d4d). +- Addition of pkgdown web site. +- Updated unit testing to from RUnit to testthat. +- Converted to roxygen2 generation of help files. +- Converted vignettes to R Markdown. +- Added Travis-CI and Appveyor support. +- Added `sequentialPValue` function. +- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv`. # gsDesign 3.0-5 (January 2018) -- Registered C routines (e52467d5). -- Fixed "gsbound" (e52467d5). -- Replaced "array" by "rep" calls to avoid `R CMD check` warnings (db229717). +- Registered C routines. +- Fixed "gsbound". +- Replaced "array" by "rep" calls to avoid `R CMD check` warnings. # gsDesign 3.0-4 (September 2017) -- First Github-based release (e52467d5). -- Cleaned up documentation for `nBinomial1Sample()` (e52467d5). -- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity (e52467d5). -- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming (e52467d5). +- First Github-based release. +- Cleaned up documentation for `nBinomial1Sample()`. +- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity. +- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming. # gsDesign 3.0-3 -- Introduced spending time as a separate concept from information time to enable concepts such as calendar-based spending functions. The only user function changed is the `gsDesign()` function and the change is the addition of the parameters `usTime` and `lsTime`; default behavior is backwards compatible (e52467d5). +- Introduced spending time as a separate concept from information time to enable concepts such as calendar-based spending functions. The only user function changed is the `gsDesign()` function and the change is the addition of the parameters `usTime` and `lsTime`; default behavior is backwards compatible. # gsDesign 3.0-2 (February 2016) -- Simplified conditional power section of gsDesignManual.pdf in doc directory (e52467d5). -- Corrected basic calculation in `gsCP()` (e52467d5). -- Eliminated deprecated ggplot2 function `opts()` (e52467d5). +- Simplified conditional power section of gsDesignManual.pdf in doc directory. +- Corrected basic calculation in `gsCP()`. +- Eliminated deprecated ggplot2 function `opts()`. # gsDesign 3.0-1 (January 2016) -- More changes to comply with R standards (in NAMESPACE - `importFrom` statements - and DESCRIPTION - adding plyr to imports) ensuring appropriate references (e52467d5). -- Deleted link in documentation that no longer exists (gsBinomialExact.Rd) (e52467d5). -- Last planned RForge-based release; moving to Github (e52467d5). +- More changes to comply with R standards (in NAMESPACE - `importFrom` statements - and DESCRIPTION - adding plyr to imports) ensuring appropriate references. +- Deleted link in documentation that no longer exists (gsBinomialExact.Rd). +- Last planned RForge-based release; moving to Github. # gsDesign 3.0-0 (December 2015) -- Updated xtable extension to meet R standards for extensions (e52467d5). -- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs (e52467d5). -- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact (e52467d5). -- Minor typo fixed in `sfLogistic` help file (e52467d5). -- Cleaned up "imports" and "depends" in an effort to be an R "good citizen" (e52467d5). -- Registered S3 methods in NAMESPACE (e52467d5). +- Updated xtable extension to meet R standards for extensions. +- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs. +- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact. +- Minor typo fixed in `sfLogistic` help file. +- Cleaned up "imports" and "depends" in an effort to be an R "good citizen". +- Registered S3 methods in NAMESPACE. # gsDesign 2.9-4 -- Minor edit to package description to comply with R standards (e52467d5). +- Minor edit to package description to comply with R standards. # gsDesign 2.9-3 (November 2014) -- Added `sfTrimmed` as likely preferred spending function approach to skipping early or all interim efficacy analyses; this also can adjust bound when final analysis is performed with less than maximum planned information. Updated `help(sfTrimmed)` to demonstrate these capabilities (e52467d5). -- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example (e52467d5). -- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects (e52467d5). -- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file (e52467d5). -- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects (e52467d5). +- Added `sfTrimmed` as likely preferred spending function approach to skipping early or all interim efficacy analyses; this also can adjust bound when final analysis is performed with less than maximum planned information. Updated `help(sfTrimmed)` to demonstrate these capabilities. +- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example. +- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects. +- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file. +- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects. From 5407088369da70dc6314e11548bf733543e238a2 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:35:30 -0700 Subject: [PATCH 19/23] Remove full stops --- NEWS.md | 105 +++++++++++++++++++++++++++----------------------------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1d03b4c6..8a1e4eaa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -498,11 +498,9 @@ We have made the spending function summary output more readable and informative. as described in the new "Binomial SPRT" vignette. - In `plot.ssrCP()`, the `hat` syntax in the mathematical expression is revised, resolving labeling issues. -- `ggplot2::qplot()` usage replaced due to its deprecation in ggplot2 3.4.0 - . +- `ggplot2::qplot()` usage replaced due to its deprecation in ggplot2 3.4.0. - Link update for the gsDesign manual in the documentation, - now directly pointing to the gsDesign technical manual bookdown project - . + now directly pointing to the gsDesign technical manual bookdown project. - Introduced a new hex sticker logo. # gsDesign 3.4.0 (October 2022) @@ -517,64 +515,63 @@ We have made the spending function summary output more readable and informative. - Addition of vignettes - Demonstrate cure model and calendar-based analysis timing for time-to-event endpoint design - Vaccine efficacy design using spending bounds and exact binomial boundary crossing probabilities - . -- Minor fix to labeling in print.gsProbability. -- Fixed error in sfStep. -- Updates to reduce R CMD check and other minor issues. +- Minor fix to labeling in print.gsProbability +- Fixed error in sfStep +- Updates to reduce R CMD check and other minor issues # gsDesign 3.2.2 (January 2022) -- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate. -- Correctly close graphics device in unit tests to avoid plot output file not found issues. -- Minor fixes to hGraph() for multiplicity graphs. -- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue. -- Minor changes to vignettes. +- Use `inherits()` instead of `is()` to determine if an object is an instance of a class, when appropriate +- Correctly close graphics device in unit tests to avoid plot output file not found issues +- Minor fixes to hGraph() for multiplicity graphs +- Minor fix to nBinomial() when odds-ratio scale specified to resolve user issue +- Minor changes to vignettes # gsDesign 3.2.1 (July 2021) -- Changed gt package usage in a vignette due to deprecated gt function. -- Replied to minor comments from CRAN reviewer (no functionality impact). -- Minor update to DESCRIPTION citing Jennison and Turnbull reference. +- Changed gt package usage in a vignette due to deprecated gt function +- Replied to minor comments from CRAN reviewer (no functionality impact) +- Minor update to DESCRIPTION citing Jennison and Turnbull reference # gsDesign 3.2.0 (January 2021) -- Substantially updated unit testing to increase code coverage above 80%. -- Updated error checking messages to print function where check fails. -- Removed dependencies on plyr packages. -- Updated github actions. +- Substantially updated unit testing to increase code coverage above 80% +- Updated error checking messages to print function where check fails +- Removed dependencies on plyr packages +- Updated github actions # gsDesign 3.1.1 (May 2020) - Vignettes updated. -- Added `hGraph()` to support ggplot2 versions of multiplicity graphs. -- Eliminated unnecessary check from `sequentialPValue`. -- Targeted release to CRAN. -- Removed dependencies on reshape2, plyr. -- Updated continuous integration. -- Updated license. +- Added `hGraph()` to support ggplot2 versions of multiplicity graphs +- Eliminated unnecessary check from `sequentialPValue` +- Targeted release to CRAN +- Removed dependencies on reshape2, plyr +- Updated continuous integration +- Updated license # gsDesign 3.1.0 (April 2019) -- Addition of pkgdown web site. -- Updated unit testing to from RUnit to testthat. -- Converted to roxygen2 generation of help files. -- Converted vignettes to R Markdown. -- Added Travis-CI and Appveyor support. -- Added `sequentialPValue` function. -- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv`. +- Addition of pkgdown web site +- Updated unit testing to from RUnit to testthat +- Converted to roxygen2 generation of help files +- Converted vignettes to R Markdown +- Added Travis-CI and Appveyor support +- Added `sequentialPValue` function +- Backwards compatible addition of spending time capabilities to `gsDesign` and `gsSurv` # gsDesign 3.0-5 (January 2018) -- Registered C routines. -- Fixed "gsbound". -- Replaced "array" by "rep" calls to avoid `R CMD check` warnings. +- Registered C routines +- Fixed "gsbound" +- Replaced "array" by "rep" calls to avoid `R CMD check` warnings # gsDesign 3.0-4 (September 2017) -- First Github-based release. -- Cleaned up documentation for `nBinomial1Sample()`. -- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity. -- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming. +- First Github-based release +- Cleaned up documentation for `nBinomial1Sample()` +- Updated documentation and code (including one default value for an argument) for `nBinomial1Sample()` to improve error handling and clarity +- Updated `sfLDOF()` to generalize with rho parameter; still backwards compatible for Lan-DeMets O'Brien-Fleming # gsDesign 3.0-3 @@ -582,9 +579,9 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.0-2 (February 2016) -- Simplified conditional power section of gsDesignManual.pdf in doc directory. -- Corrected basic calculation in `gsCP()`. -- Eliminated deprecated ggplot2 function `opts()`. +- Simplified conditional power section of gsDesignManual.pdf in doc directory +- Corrected basic calculation in `gsCP()` +- Eliminated deprecated ggplot2 function `opts()` # gsDesign 3.0-1 (January 2016) @@ -594,21 +591,21 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.0-0 (December 2015) -- Updated xtable extension to meet R standards for extensions. -- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs. -- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact. -- Minor typo fixed in `sfLogistic` help file. -- Cleaned up "imports" and "depends" in an effort to be an R "good citizen". -- Registered S3 methods in NAMESPACE. +- Updated xtable extension to meet R standards for extensions +- Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs +- Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact +- Minor typo fixed in `sfLogistic` help file +- Cleaned up "imports" and "depends" in an effort to be an R "good citizen" +- Registered S3 methods in NAMESPACE # gsDesign 2.9-4 -- Minor edit to package description to comply with R standards. +- Minor edit to package description to comply with R standards # gsDesign 2.9-3 (November 2014) - Added `sfTrimmed` as likely preferred spending function approach to skipping early or all interim efficacy analyses; this also can adjust bound when final analysis is performed with less than maximum planned information. Updated `help(sfTrimmed)` to demonstrate these capabilities. -- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example. -- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects. -- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file. -- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects. +- Added `sfGapped`, which is primarily intended to eliminate futility analyses later in a study; see `help(sfGapped)` for an example +- Added `summary.spendfn()` to provide textual summary of spending functions; this simplified the print function for gsDesign objects +- Added `sfStep()` which can be used to set an interim spend when the exact amount of information is unknown; an example of how this can be misused is provided in the help file +- Fixed rounding so that `gsBoundSummary`, `xtable.gsSurv` and `summary.gsDesign` are consistent for `gsSurv` objects From 162097a7eed82d78748476d6eaee983e245f6660 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:36:18 -0700 Subject: [PATCH 20/23] Remove and add full stops --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8a1e4eaa..59f1563a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -542,7 +542,7 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.1.1 (May 2020) -- Vignettes updated. +- Vignettes updated - Added `hGraph()` to support ggplot2 versions of multiplicity graphs - Eliminated unnecessary check from `sequentialPValue` - Targeted release to CRAN @@ -591,7 +591,7 @@ We have made the spending function summary output more readable and informative. # gsDesign 3.0-0 (December 2015) -- Updated xtable extension to meet R standards for extensions +- Updated xtable extension to meet R standards for extensions. - Fixed `xtable.gsSurv` and `print.gsSurv` to work with 1-sided designs - Update to calls to ggplot to replace show_guide (deprecated) with `show.legend` arguments where used in `ggplot2::geom_text` calls; no user impact - Minor typo fixed in `sfLogistic` help file From 1e2910dc531ad1d58d5334fff6e072a7f2f50be9 Mon Sep 17 00:00:00 2001 From: Nan Xiao Date: Tue, 26 May 2026 23:38:15 -0700 Subject: [PATCH 21/23] Use the correct PR number --- NEWS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 59f1563a..eaba288d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -76,7 +76,7 @@ accrual rate and duration when both `T` and `minfup` are `NULL`, solving follow-up duration against the final group-sequential event requirement. This allows Schoenfeld survival designs to reproduce SAS PROC SEQDESIGN's - fixed-accrual follow-up solve (#265). + fixed-accrual follow-up solve (#270). - `simBinomialSeasonalExact()` now stops simulated trials at the first efficacy or futility boundary crossing for reporting stopping time, total events, and total enrollment, while preserving the non-binding futility @@ -138,7 +138,7 @@ `test.type = 2`, `alpha = 0.025`, `method = "Schoenfeld"`, and `T = minfup = NULL` to match SAS's symmetric two-sided fixed-accrual design, and by separating fractional-time output from the SAS ceiling-time - adjusted design (#265). + adjusted design (#270). - Added vignette "Multi-season studies for rare events" (`vignette("MultiSeasonRareEvents")`) demonstrating exact-binomial seasonal monitoring, analysis-time bound updates via @@ -173,7 +173,7 @@ equal-allocation path (#264). - Expanded `nSurv()` and `gsSurv()` regression tests across the supported `T`/`minfup` timing combinations for Schoenfeld, Freedman, and - Bernstein-Lagakos methods (#265). + Bernstein-Lagakos methods (#270). # gsDesign 3.9.0 (February 2026) From 4ff9595d6499a16dd574c4e7cb5d67f0436219eb Mon Sep 17 00:00:00 2001 From: keaven Date: Wed, 27 May 2026 06:31:06 -0400 Subject: [PATCH 22/23] Clarify exact SAS reproduction workflow in survival vignette. Co-authored-by: Cursor --- vignettes/SeqDesignSurvival.Rmd | 90 ++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 29 deletions(-) diff --git a/vignettes/SeqDesignSurvival.Rmd b/vignettes/SeqDesignSurvival.Rmd index 02a3cfc7..3c14373f 100644 --- a/vignettes/SeqDesignSurvival.Rmd +++ b/vignettes/SeqDesignSurvival.Rmd @@ -17,15 +17,25 @@ knitr::opts_chunk$set( ) ``` +```{r, echo=FALSE, results='asis'} +if (knitr::is_html_output()) { + knitr::asis_output( + "" + ) +} +``` + ```{r} library(gsDesign) ``` ## Overview -This vignette provides a thorough analysis of the differences between -the SAS SEQDESIGN procedure and the R package gsDesign for designing -group sequential clinical trials with time-to-event endpoints. +This vignette emphasizes exact numerical reproduction of SAS PROC +SEQDESIGN survival outputs in gsDesign when design assumptions are +matched. +We first reproduce the SAS fractional-time table to printed precision. +We then show different options that may lead to confusion in translating SAS PROC SEQDESIGN calls to gsDesign. ### Starting point: SAS PROC SEQDESIGN survival example @@ -35,17 +45,27 @@ The first PROC SEQDESIGN call in that example does not specify `ACCTIME`; SAS therefore derives a range of possible accrual times. The comparison below uses the subsequent SAS call with `ACCTIME=18`, which fixes the maximum sample size at `15 * 18 = 270` subjects and asks SAS to solve the follow-up time. +Note that the SAS survival sample size model is specified as uses the Schoenfeld formula for the targeted event counts. +The computed sample size and event counts at each analysis are continuous values, not rounded to integers. ```sas -proc seqdesign; - ErrorSpend: design nstages=4 method=errfuncobf +proc seqdesign; /* Group sequential design procedure */ + ErrorSpend: design /* Label for this design block */ + nstages=4 /* Total analyses including final */ + method=errfuncobf /* Lan-DeMets O'Brien-Fleming spending */ + alt=twosided /* Two-sided alternative hypothesis */ + stop=reject /* Early stop only for efficacy */ + alpha=0.05 /* Total two-sided Type I error */ + beta=0.10 /* Type II error (90% power) */ ; - samplesize model(ceiladjdesign=include)=twosamplesurvival( - nullhazard = 0.03466 - hazard = 0.01733 - accrual = uniform - accrate = 15 - acctime = 18); + samplesize model(ceiladjdesign=include)= /* Survival model */ + twosamplesurvival( /* Two-sample survival endpoint */ + nullhazard = 0.03466 /* Control hazard under H0 */ + hazard = 0.01733 /* Experimental hazard under H1 */ + accrual = uniform /* Uniform enrollment */ + accrate = 15 /* Subjects enrolled per time unit */ + acctime = 18 /* Accrual duration in time units */ + ); run; ``` @@ -93,45 +113,42 @@ sas_ceiling_time <- data.frame( ) ``` -gsDesign uses one-sided alpha by default, so we need to convert the two-sided SAS alpha to a one-sided alpha. -The two-sided alpha is 0.05, so the one-sided alpha is 0.025. +To reproduce the SAS fractional-time output exactly while specifying +analysis timing in calendar units, we use +`gsDesign::gsSurvCalendar(calendarTime = sas_fractional$Calendar_time)`. +We keep one-sided `alpha = 0.025`, use `method = "Schoenfeld"`, and keep +accrual fixed at 15 subjects per time unit for 18 time units by setting +`R = 18` with `minfup = max(calendarTime) - R`. ```{r aligned-r-attempt} -des <- gsSurv( - k = 4, +des <- gsSurvCalendar( test.type = 2, alpha = 0.025, beta = 0.10, sfu = sfLDOF, # Lan-DeMets O'Brien--Fleming - timing = c(.25, .50, .75, 1), # Equal information fractions + calendarTime = sas_fractional$Calendar_time, + spending = "information", lambdaC = 0.03466, hr = 0.5, eta = 0, # No dropout gamma = 15, # Uniform accrual rate of 15/month R = 18, # Accrual duration (months) - T = NULL, - minfup = NULL, + minfup = max(sas_fractional$Calendar_time) - 18, ratio = 1, # 1:1 randomization method = "Schoenfeld" ) -des |> gsBoundSummary(tdigits = 2, ddigits = 2) |> gt::gt() +# des |> gsBoundSummary(tdigits = 2, ddigits = 2) |> gt::gt() ``` -This call now matches the SAS fractional-time design. The key inputs are -`alpha = 0.025`, which represents the one-sided alpha corresponding to SAS's -two-sided total alpha of 0.05; `method = "Schoenfeld"`, which matches the SAS -event formula; and `T = NULL, minfup = NULL`, which fixes the accrual duration -and solves the follow-up time. ### Side-by-side comparison table ```{r comparison-table} -events_a2 <- round(des$n.I, 2) - -z_a2 <- round(des$upper$bound, 4) - -N2a <- sum(des$eNC[4, ]) + sum(des$eNE[4, ]) +# Get results for comparison from gsDesign object +events_a2 <- round(des$n.I, 2) # Event counts +z_a2 <- round(des$upper$bound, 4) # Z-boundaries +N2a <- sum(des$eNC[4, ]) + sum(des$eNE[4, ]) # Total sample size knitr::kable( data.frame( @@ -191,6 +208,9 @@ and solves for the additional follow-up time. To match this in `gsSurv()`, set both `T = NULL` and `minfup = NULL`. This tells `gsSurv()` to keep the input accrual rate and accrual duration fixed, then solve the follow-up duration needed for the final group sequential event requirement. +If analysis times are specified directly in calendar units, the corresponding +`gsSurvCalendar()` setup is `calendarTime = ...` with +`minfup = max(calendarTime) - accrual_duration`. This is a common source of apparent disagreement. For a fixed-duration survival design, `T` is the total study duration and `minfup` is the minimum @@ -222,6 +242,7 @@ knitr::kable( Quantity = c( "Two-sided Type I error", "Symmetric two-sided design", + "Analysis timing input", "Event formula", "Accrual duration", "Total study duration", @@ -230,6 +251,7 @@ knitr::kable( SAS = c( "alpha = 0.05 total", "Early stop to reject either side", + "Calendar times reported by PROC output", "Schoenfeld log-rank information", "ACCTIME = 18", "Total Time = 25.13323", @@ -238,6 +260,7 @@ knitr::kable( gsDesign = c( "alpha = 0.025 per tail", "test.type = 2", + "gsSurvCalendar(calendarTime = ...)", "method = \"Schoenfeld\"", "R = 18", "T = NULL", @@ -246,6 +269,7 @@ knitr::kable( Reason = c( "gsDesign stores and spends one-sided alpha", "Mirrors the upper and lower efficacy boundaries", + "Uses direct calendar-time analysis specification", "Avoids Lachin-Foulkes default event calculation", "Keeps the same fixed accrual duration", "Lets gsSurv() solve total time from fixed accrual", @@ -259,6 +283,10 @@ knitr::kable( ## Aligning the two approaches +The exact fractional-time reproduction above uses `gsSurvCalendar()` because +SAS reports analysis timing in calendar units. An equivalent information-time +translation using `gsSurv()` is shown next. + ### `gsSurv()` with aligned parameters Let's start our work with gsDesign by defining parameters to match the SAS @@ -404,6 +432,10 @@ The SAS example also reports a `CEILING=TIME` adjusted design. This is not the same design as the equal-information fractional-time output above. The analysis times are rounded up to 12, 17, 21, and 26, which increases the expected events and changes the information fractions. +For calendar-scheduled analyses in general, this would naturally be specified +with `gsSurvCalendar(calendarTime = c(12, 17, 21, 26), spending = "information")`. +To match the SAS `CEILING=TIME` table exactly, we retain fixed accrual and use +the implied event counts at those rounded calendar times. We can reproduce the ceiling-time event counts directly from the fixed accrual rate, fixed accrual duration, and exponential event rates. With equal From 7d8892c981a193c8d94daef4b825d949e889dd5d Mon Sep 17 00:00:00 2001 From: Keaven Date: Wed, 27 May 2026 10:57:07 -0400 Subject: [PATCH 23/23] Fix double toInteger conversion in seasonal simulation (#264) --- DESCRIPTION | 1 + R/simBinomialSeasonalExact.R | 2 +- man/gsDesign-package.Rd | 5 ----- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 789c64dc..06f80698 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -47,3 +47,4 @@ Suggests: VignetteBuilder: knitr Config/roxygen2/version: 8.0.0 +RoxygenNote: 7.3.3 diff --git a/R/simBinomialSeasonalExact.R b/R/simBinomialSeasonalExact.R index 00779c26..f8ebd9f6 100644 --- a/R/simBinomialSeasonalExact.R +++ b/R/simBinomialSeasonalExact.R @@ -164,7 +164,7 @@ simBinomialSeasonalExact <- function( stop("gsD$ratio must be a positive scalar for simulation", call. = FALSE) } - gsD_int <- toInteger(gsD) + gsD_int <- if (max(round(gsD$n.I) != gsD$n.I)) toInteger(gsD) else gsD if (is.null(planned_counts)) { if (is.null(timing)) { timing <- gsD_int$timing diff --git a/man/gsDesign-package.Rd b/man/gsDesign-package.Rd index 4c4ff9c3..3c4b5a84 100644 --- a/man/gsDesign-package.Rd +++ b/man/gsDesign-package.Rd @@ -21,11 +21,6 @@ Useful links: \author{ \strong{Maintainer}: Keaven Anderson \email{keaven_anderson@merck.com} -Authors: -\itemize{ - \item Keaven Anderson \email{keaven_anderson@merck.com} -} - Other contributors: \itemize{ \item Merck & Co., Inc., Rahway, NJ, USA and its affiliates (\href{https://ror.org/02891sr49}{ROR}) [copyright holder]