Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c1065ea
Fix seasonal exact binomial simulation updates
keaven May 15, 2026
30daa5f
Add cross-agent workflow instructions
keaven May 15, 2026
04cc792
Document coding agent instruction support
keaven May 15, 2026
dbfa702
Ignore local agent directories in R builds
keaven May 15, 2026
8dd96c3
Add AI skills vignette
keaven May 15, 2026
b1eae2f
Clarify AI design function selection
keaven May 15, 2026
f577a15
Prepare gsDesign 3.10.0 release
keaven May 25, 2026
2e9260b
Use integer calendar design in multi-season vignette
keaven May 25, 2026
327fe64
Merge remote-tracking branch 'origin/master' into 264-simbinomialseas…
keaven May 25, 2026
1745859
Allow null VE scenarios in seasonal simulation
keaven May 25, 2026
4372fe2
Align survival timing solves with SAS
keaven May 26, 2026
9e77893
Document SAS survival alpha convention
keaven May 26, 2026
51aac8f
Remove duplicated AI slop files
nanxstats May 27, 2026
dc03128
Split part of AGENTS.md into survival design routing skill
nanxstats May 27, 2026
c24273a
Reorder build ignore items
nanxstats May 27, 2026
3096822
Reset URL to the correct address
nanxstats May 27, 2026
67eac0b
Reset version number to 3.9.0.9006
nanxstats May 27, 2026
7ee1f40
Run roxygen2
nanxstats May 27, 2026
73113ed
Clean up unnecessary commit hashes and changelog entries
nanxstats May 27, 2026
5407088
Remove full stops
nanxstats May 27, 2026
162097a
Remove and add full stops
nanxstats May 27, 2026
1e2910d
Use the correct PR number
nanxstats May 27, 2026
4ff9595
Clarify exact SAS reproduction workflow in survival vignette.
keaven May 27, 2026
7d8892c
Fix double toInteger conversion in seasonal simulation (#264)
keaven May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
^revdep$
tests/benchmarks
^\.agents$
^AGENTS\.md$
23 changes: 23 additions & 0 deletions .agents/skills/survival-design-routing/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 64 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# AGENTS.md

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 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)`.

## 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

- 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 <branch-name>`.
Do not push to `main` directly.
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: gsDesign
Version: 3.9.0.9005
Version: 3.9.0.9006
Title: Group Sequential Design
Authors@R: c(
person("Keaven", "Anderson", email = "keaven_anderson@merck.com", role = c("aut", "cre")),
Expand Down Expand Up @@ -47,3 +47,4 @@ Suggests:
VignetteBuilder:
knitr
Config/roxygen2/version: 8.0.0
RoxygenNote: 7.3.3
89 changes: 70 additions & 19 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
`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 (&plusmn;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).
Expand Down Expand Up @@ -34,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
(18 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
Expand All @@ -46,41 +46,80 @@
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.
A feasibility check verifies that the implied experimental-arm event rates
(`control_event_rate * (1 - ve)`) remain in `[0, 1)` (#267).

## 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 (#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
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.
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,
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).
- 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

- 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,
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).
Expand All @@ -94,35 +133,47 @@
- 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 (#270).
- 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).

## 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 (#270).

# gsDesign 3.9.0 (February 2026)

Expand Down
108 changes: 108 additions & 0 deletions R/gsSurv-method.R
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading