---
title: "Power Computation for Group Sequential Survival Designs"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Power Computation for Group Sequential Survival Designs}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include=FALSE}
library(gsDesign)
```

## Motivation

`gsSurvPower()` has two primary uses:

1. **Compute achieved power without deriving sample size.** Given enrollment,
   failure, dropout, treatment-effect, and analysis-timing assumptions, what
   power does the design achieve?
2. **Evaluate scenarios for an existing design.** What happens when enrollment,
   failure rates, the hazard ratio, or operational timing rules differ from
   plan?

This reverses the usual role of `gsSurv()` and `gsSurvCalendar()`, which derive
sample sizes or enrollment rates to achieve target power.

Common scenarios include:

- **Sensitivity analysis**: What happens to power if the true hazard ratio
  is 0.75 instead of the design assumption of 0.65?
- **Changing alpha**: What if the multiplicity scheme initially allocates
  $\alpha = 0.0125$ and later allows $\alpha = 0.025$?
- **Modified enrollment**: What if enrollment is slower than planned?
- **Modified failure or dropout rates**: What if events accumulate faster or
  slower than planned?
- **Different analysis times**: What if interim analyses occur at calendar
  times that differ from the original design?

`gsSurvPower()` addresses these questions by computing power for a group
sequential survival design under user-specified assumptions.
For deriving the initial survival sample size design, see
`vignette("gsSurvBasicExamples")`. For reproducing SAS PROC SEQDESIGN survival
sample size output, see `vignette("SeqDesignSurvival")`.

## Quick start

If you already have a design object, the most common workflow is to reuse its
defaults and override only the assumptions you want to stress-test.

```{r gssurvpower_quick_start}
design <- gsSurv(
  k = 3, test.type = 4, alpha = 0.025, sided = 1, beta = 0.1,
  sfu = sfHSD, sfupar = -4, sfl = sfHSD, sflpar = -2,
  lambdaC = log(2) / 12, hr = 0.7, hr0 = 1,
  eta = 0.01, gamma = 10, R = 16, minfup = 12, T = 28
)

pwr_design <- gsSurvPower(x = design, plannedCalendarTime = design$T)
pwr_design$power
```

The returned object contains the same design-style structure as `gsSurv()`, but
with `variable = "Power"`. The most useful components to inspect first are:

- `pwr_design$power` for overall power.
- `pwr_design$n.I` for expected events at each analysis.
- `pwr_design$T` for analysis times.
- `pwr_design$timing` for information fractions.
- `pwr_design$upper$bound` and `pwr_design$lower$bound` for the bounds being
  applied.

### What this calculation does—and does not do

`gsSurvPower()` applies asymptotic group sequential calculations to expected
enrollment, dropout, event accumulation, and analysis timing under one set of
assumptions. Thus, an event- or enrollment-triggered analysis time is the
*expected* time at which its rule is met.

It does not simulate trial-to-trial variation in enrollment, failure, dropout,
or operational analysis timing. Use simulation when that variability may
materially affect operating characteristics, such as the probability of
meeting competing timing rules, the distribution of analysis dates, or power
under realistic trial execution. Packages such as
[**simtrial**](https://merck.github.io/simtrial/) can support that more
comprehensive evaluation.

## How gsSurvPower uses your inputs

`gsSurvPower()` accepts an optional `gsSurv`-class object `x`, including output
from `gsSurv()` and `gsSurvCalendar()`, that provides defaults for all
parameters. Any parameter the user explicitly specifies overrides the
corresponding value from `x`. When `x` is not provided, all design parameters
must be specified directly.

### Hazard ratio roles

Two distinct hazard ratios play different roles:

- **`hr`**: The assumed hazard ratio under which power is computed. This is the
  "what-if" treatment effect.
- **`hr1`**: The design hazard ratio used to calibrate futility bounds
  (`test.type` 3, 4, 7, 8 only; not used for `test.type` 5, 6 or harm bounds).
  When `x` is provided, `hr1` defaults to `x$hr` (the effect size the trial was
  originally designed for). Futility bounds remain calibrated to the design
  assumption even when power is evaluated under a different `hr`.

### Beta spending in scenario analyses

Beta spending is a design rule for constructing futility bounds, not a promise
that every scenario evaluated will have the design's Type II error. It helps to
separate two quantities:

- **Design beta** (`x$beta`) is allocated across analyses by `sfl`, `sflpar`,
  and the lower spending-time vector. For beta-spending designs, the futility
  bounds are calibrated under `hr1`, the design alternative.
- **Achieved beta** (`1 - power`, returned as `beta`) is the probability of not
  rejecting the null under the scenario's `hr`, enrollment, failure, dropout,
  and analysis timing. It can be much larger or smaller than design beta.

For `test.type = 4`, the non-binding designation means that the futility bound
is ignored when controlling Type I error. It remains an operational stopping
bound and therefore affects achieved power. The same distinction between
design beta and achieved beta applies to the beta-spending futility bounds in
`test.type` 3, 7, and 8. Test types 5 and 6 instead spend lower-bound
probability under the null, and harm spending for test types 7 and 8 is a
separate null-based calculation.

When a reference design `x` is supplied, futility-bound handling follows these
rules:

| Scenario change | Futility-bound handling |
| --- | --- |
| Information fractions and spending inputs unchanged | Reuse the bounds from `x` |
| Only alpha or the upper spending function changes, with timing unchanged | Preserve the bounds from `x`, clipping only if a lower bound exceeds the new efficacy bound |
| Information fractions or spending times change | Recompute the bounds on the new schedule using design beta, `hr1`, `sfl`, and `sflpar` |
| `informationRates`, calendar or planned-versus-actual spending, or `lsTime` is supplied | Derive the selected lower effective spending-time vector and recompute the bounds |

Thus, changing `hr` with fixed event targets generally preserves the original
information fractions and futility bounds. Changing `hr` at fixed calendar
times can change expected event fractions, causing the bounds to be
recalibrated on the new information schedule while retaining the original
design-beta calibration.

For instance, the following comparison evaluates the design assumptions and
then HR = 0.8 with either fixed event targets or fixed calendar times. The
target beta spending remains 0.10, but achieved beta is much higher under the
weaker treatment effect. Fixed events preserve the original bounds; fixed
calendar times slightly change expected information fractions and therefore
the recalibrated bounds.

```{r beta_spending_scenarios}
beta_cases <- list(
  `Design assumptions` = pwr_design,
  `HR 0.8, fixed events` = gsSurvPower(
    x = design, hr = 0.8, targetEvents = design$n.I
  ),
  `HR 0.8, fixed calendar` = gsSurvPower(
    x = design, hr = 0.8, plannedCalendarTime = design$T
  )
)

collapse_values <- function(x) paste(round(x, 3), collapse = ", ")

data.frame(
  Scenario = names(beta_cases),
  Information_Fractions = vapply(
    beta_cases, function(x) collapse_values(x$timing), character(1)
  ),
  Futility_Bounds = vapply(
    beta_cases, function(x) collapse_values(x$lower$bound), character(1)
  ),
  Target_Beta_Spending = vapply(
    beta_cases, function(x) sum(x$lower$spend), numeric(1)
  ),
  Achieved_Beta = vapply(beta_cases, function(x) x$beta, numeric(1))
)
```

### Analysis timing: calendar time vs. event-driven

Analysis times can be specified by calendar time, by target event counts, or
by a combination of criteria. The choice has an important consequence for
sensitivity analyses:

- **`plannedCalendarTime`** fixes the calendar time when used alone. When
  combined with other timing rules, it supplies an earliest time or floor.
  Expected events are then recomputed under the assumed HR.
  A worse HR (closer to 1) produces *more* expected events at the same
  calendar time because the experimental arm fails faster.
  This gives an "unconditional" power that reflects how the assumed
  treatment effect influences event accrual.

- **`targetEvents`** fixes the overall event count at each analysis. The
  calendar time is the time until expected events reach the target under the
  assumed HR. Since the event counts are held
  constant, the information fractions do not change with HR, and the
  resulting power matches the `gsDesign` power plot (`plot(x, plottype = 2)`)
  to numerical precision.

Both modes are useful. Calendar-time analyses are natural when the protocol
specifies analysis dates; event-driven analyses are natural when the protocol
specifies event targets.

When both `plannedCalendarTime` and `targetEvents` are specified,
`plannedCalendarTime` is a floor rather than an exact analysis time. If the
expected event target is reached before that time, the analysis remains at the
planned calendar time and may include more events than targeted. If the target
has not been reached, the analysis is delayed until it is reached. Supplying
`maxExtension` caps that delay at
`plannedCalendarTime + maxExtension`; when the cap is reached first, the
analysis proceeds with fewer expected events than targeted. Other applicable
floor criteria can require an even later analysis, subject to the same hard
extension cap.

`maxCalendarTime` supplies a different kind of deadline: it is an absolute
calendar-time cap rather than an extension measured from a timing anchor.
After combining all floor and event requirements, the expected analysis time
is capped at `maxCalendarTime`. If both `maxExtension` and `maxCalendarTime`
are supplied, the earlier deadline applies. This distinction preserves the
existing meaning of `maxExtension` while matching the absolute-cap behavior of
simtrial's `max_extension_for_target_event`.

### Quick decision guide

| Operational rule | Argument | Interpretation |
| --- | --- | --- |
| Analyze at a calendar time | `plannedCalendarTime` | Exact time alone; earliest time when combined |
| Analyze after an overall event target | `targetEvents` | Expected time the overall target is reached |
| Require event targets by stratum | `targetEventsPerStratum` | Earliest time every active stratum target is reached |
| Require spacing between looks | `minTimeFromPreviousAnalysis` | Earliest time relative to the previous analysis |
| Require enrollment plus follow-up | `minN`, `minFollowUp` | Earliest time satisfying both criteria |
| Require enrollment by stratum plus follow-up | `minNPerStratum`, `minFollowUp` | Earliest time every active stratum requirement plus follow-up is met |
| Require final minimum follow-up | explicit `minfup` | Final-analysis floor after enrollment ends |
| Cap delay relative to a timing anchor | `maxExtension` | Relative hard cap preserving the existing gsDesign convention |
| Stop at an absolute calendar deadline | `maxCalendarTime` | Absolute hard cap, corresponding to simtrial cut behavior |

For scenario analyses, override only the assumptions that change:

| Scenario | Arguments commonly changed |
| --- | --- |
| Treatment effect | `hr` |
| Control failure rates | `lambdaC`, `S` |
| Dropout | `eta`, `etaE` |
| Enrollment rate or shape | `gamma` |
| Enrollment duration or target size | `R`, or `targetN` |
| Operational analysis rules | Timing arguments in the table above |

Per-analysis timing rules can be combined, each specified as a scalar
(recycled to all `k` analyses) or a vector of length `k` with `NA` for
"not applicable":

- `plannedCalendarTime`: Calendar-time floor for the analysis.
- `targetEvents`: Overall event requirement for the analysis.
- `maxExtension`: Maximum time beyond the floor to wait for target events.
- `maxCalendarTime`: Absolute calendar-time deadline for an analysis.
- `minTimeFromPreviousAnalysis`: Minimum elapsed time since the previous analysis.
- `minN`: Minimum sample size enrolled before analysis.
- `minNPerStratum`: Matrix of minimum enrollment by analysis and stratum;
  `NA` deactivates only the corresponding stratum requirement.
- `minFollowUp`: Additional follow-up after active `minN` or
  `minNPerStratum` requirements are reached. `NA` means no additional
  follow-up, while a non-missing value requires an enrollment threshold at
  the same analysis.
- `targetEventsPerStratum`: Matrix of event requirements by analysis and
  stratum; `NA` deactivates only the corresponding stratum requirement.

Each analysis must retain at least one active floor or event requirement.
The mixed-`NA` convention is specific to timing rules. For selective bounds,
use `FALSE` rather than `NA` in `testUpper`, `testLower`, or `testHarm`.
Spending-time inputs (`informationRates`, `usTime`, and `lsTime`) describe a
complete spending schedule and therefore must not contain `NA`.

When multiple criteria apply to a single analysis, the analysis time is the
maximum of all floor criteria and all overall or per-stratum event-target
times. Thus, active requirements use AND logic. `maxExtension` defines a hard
deadline: the analysis time never
exceeds `plannedCalendarTime + maxExtension` (or `T[i-1] + maxExtension`
when no calendar time is specified), even if other criteria such as
`minTimeFromPreviousAnalysis` or enrollment plus follow-up would push it
later. `maxCalendarTime` is then applied as an absolute cap.

### Relationship to simtrial analysis cuts

The timing grammar intentionally parallels
[`simtrial::get_analysis_date()`](https://merck.github.io/simtrial/reference/get_analysis_date.html),
with camel-case names and one value per planned analysis. The main distinction
is conceptual: `gsSurvPower()` applies the rules to expected event and
enrollment counts, whereas simtrial applies them to a realized simulated data
set. Consequently, matching rules do not imply identical analysis dates in any
one simulated trial.

| simtrial cut argument | `gsSurvPower()` argument | Relationship |
| --- | --- | --- |
| `planned_calendar_time` | `plannedCalendarTime` | Calendar-time floor when combined |
| `target_event_overall` | `targetEvents` | Overall event requirement |
| `target_event_per_stratum` | `targetEventsPerStratum` | Per-stratum requirement; rows index analyses and columns index strata |
| `min_time_after_previous_analysis` | `minTimeFromPreviousAnalysis` | Minimum gap from the preceding analysis |
| `min_n_overall` | `minN` | Overall enrollment requirement |
| `min_n_per_stratum` | `minNPerStratum` | Per-stratum enrollment requirement; rows index analyses and columns index strata |
| `min_followup` | `minFollowUp` | Follow-up after all active enrollment requirements |
| `max_extension_for_target_event` | `maxCalendarTime` | Absolute calendar cap, despite the simtrial argument's name |

simtrial specifies one cut at a time and therefore uses a named vector for its
per-stratum requirements. `gsSurvPower()` specifies all `k` analyses in one
call and uses a `k`-by-`nstrata` matrix; use `NA` for an inactive stratum at a
look. `maxExtension` remains available for the older gsDesign relative-cap
convention and has no exact simtrial counterpart.

### Common timing pitfalls

The timing arguments in `gsSurvPower()` describe a fixed operational plan.
They do not solve the study design to achieve power the way `gsSurv()` does.
Several inputs are therefore easy to misread.

**`minfup` is a final-analysis floor, not a target event rule.** If `minfup`
is explicitly supplied, the final analysis cannot occur before enrollment has
ended and the last enrolled subject has at least that much follow-up. For an
event-driven design with enrollment durations `R`, the final analysis time is
at least `sum(R) + minfup`. If the requested final `targetEvents` is expected
earlier, the final analysis remains delayed to satisfy minimum follow-up, and
the expected final event count may exceed the event target.

```{r timing_pitfall_minfup}
pwr_minfup <- gsSurvPower(
  k = 3, test.type = 1, alpha = 0.025, sided = 1,
  sfu = sfLDOF, sfupar = NULL,
  lambdaC = log(2) / 15, hr = 0.7, hr0 = 1,
  eta = 0.001,
  gamma = c(1, 2, 3, 4) * 500 /
    sum(c(1, 2, 3, 4) * c(2, 2, 2, 6)),
  R = c(2, 2, 2, 6),
  targetEvents = c(100, 200, 300),
  minfup = 25,
  ratio = 1.5,
  testUpper = TRUE, testLower = FALSE
)

data.frame(
  Analysis = 1:3,
  Time = round(pwr_minfup$T, 1),
  Events = round(pwr_minfup$n.I, 1)
)
```

**`targetN` changes enrollment duration, not enrollment rates.** Use
`targetN` when `gamma` gives relative or absolute enrollment rates and you
want `gsSurvPower()` to rescale `R` so that `sum(gamma * R) == targetN`.
Do not also specify a non-`NULL` `R`. If the protocol fixes enrollment
duration, specify `R` and scale `gamma` yourself.

```{r timing_pitfall_targetn}
pwr_target_n <- gsSurvPower(
  k = 2, test.type = 1, alpha = 0.025, sided = 1,
  lambdaC = log(2) / 15, hr = 0.7, hr0 = 1,
  eta = 0.001,
  gamma = c(10, 20, 30, 40),
  targetN = 500,
  plannedCalendarTime = c(24, 36),
  ratio = 1.5,
  testUpper = TRUE, testLower = FALSE
)

pwr_target_n$R
sum(rowSums(pwr_target_n$gamma) * pwr_target_n$R)
```

**`maxExtension` needs a floor time.** `maxExtension` is the amount of time
the analysis may wait beyond a floor criterion while waiting for
`targetEvents`. A floor can come from `plannedCalendarTime`,
`minTimeFromPreviousAnalysis`, `minN`, or an explicitly supplied final
`minfup`. Without a floor, `maxExtension` is not meaningful.

```{r timing_pitfall_maxextension}
pwr_extension <- gsSurvPower(
  k = 3, test.type = 1, alpha = 0.025, sided = 1,
  lambdaC = log(2) / 15, hr = 0.7, hr0 = 1,
  eta = 0.001,
  gamma = c(1, 2, 3, 4) * 500 /
    sum(c(1, 2, 3, 4) * c(2, 2, 2, 6)),
  R = c(2, 2, 2, 6),
  plannedCalendarTime = c(24, 30, 36),
  targetEvents = c(200, 300, 400),
  maxExtension = c(0, 6, 6),
  ratio = 1.5,
  testUpper = TRUE, testLower = FALSE
)

round(pwr_extension$T, 1)
```

**`maxCalendarTime` is an absolute date, not a duration.** Use it when the
operational rule is "analyze no later than month 30," including when there is
an event requirement but no natural floor from which to measure an extension.
If a target or floor is not reached by that date, the analysis proceeds at the
cap under the expected-value calculation. This matches how simtrial applies
`max_extension_for_target_event`, despite the latter argument's name. When a
relative extension and an absolute deadline are both part of the plan, supply
both cap arguments; the earlier one controls.

**`minN + minFollowUp` can define analysis timing, but analyses must remain
strictly increasing.** This pair says: wait until at least `minN` subjects
are enrolled, then wait `minFollowUp` additional time. It can be used as the
primary timing rule, but the resulting analysis times and expected event
totals must increase across analyses. If two analyses use the same enrollment
threshold and follow-up rule, add `plannedCalendarTime`, `targetEvents`, or
`minTimeFromPreviousAnalysis` to separate them.

For a stratified plan, replace or supplement `minN` with a
`minNPerStratum` matrix. The follow-up clock starts after the last active
overall or per-stratum enrollment threshold is reached.

```{r timing_pitfall_minn}
pwr_min_n <- gsSurvPower(
  k = 3, test.type = 1, alpha = 0.025, sided = 1,
  lambdaC = log(2) / 15, hr = 0.7, hr0 = 1,
  eta = 0.001,
  gamma = c(1, 2, 3, 4) * 500 /
    sum(c(1, 2, 3, 4) * c(2, 2, 2, 6)),
  R = c(2, 2, 2, 6),
  minN = c(100, 300, 500),
  minFollowUp = c(6, 6, 6),
  ratio = 1.5,
  testUpper = TRUE, testLower = FALSE
)

data.frame(
  Analysis = 1:3,
  Time = round(pwr_min_n$T, 1),
  N = round(pwr_min_n$N, 1),
  Events = round(pwr_min_n$n.I, 1)
)
```

### Spending and method

- **`spending`**: One of `"information"` (default), `"calendar"`, or
  `"min_planned_actual"`.
  Information-based spending tracks the fraction of statistical information
  accumulated; calendar-based spending sets `usTime = lsTime = T / max(T)`.
  With a reference design `x`, planned-versus-actual spending sets both
  spending-time vectors to
  `pmin(x$n.I, actual events) / x$n.I[k]`, matching simtrial's
  `ia_alpha_spending = "min_planned_actual"` convention.
  Custom spending times can also be passed via `usTime` and `lsTime`, but they
  are ignored for the latter two modes.

- **`informationRates`**: Planned information-fraction caps (complete vector
  of length `k`, without missing values). At each analysis, the effective
  spending time is
  `pmin(informationRates, actual_timing)`. Thus, spending cannot run ahead of
  either the planned information schedule or the information actually
  accumulated. `informationRates` takes precedence over `spending`, `usTime`,
  and `lsTime`; upper and lower spending both use the same effective
  spending-time vector.

- **`fullSpendingAtFinal`**: When `TRUE`, the final element of the
  upper and lower spending-time vectors is forced to 1 after applying
  `informationRates`, calendar spending, or user-supplied `usTime` /
  `lsTime`. This is useful when a selected spending-time vector would
  otherwise leave either upper- or lower-bound spending incomplete.

- **`method`**: One of `"LachinFoulkes"` (default), `"Schoenfeld"`,
  `"Freedman"`, or `"BernsteinLagakos"`. Controls how fixed-design events
  (`n.fix`) and drift parameter $\theta$ are computed when `x` is not provided.
  When `x` is provided, `x$n.fix` and, implicitly, $\theta$ are used directly for
  exact consistency with the design.

### Stratified timing arguments

`targetEvents` accepts a scalar (recycled), a vector of length `k`
(one overall target per analysis). Use `targetEventsPerStratum` for a matrix
with `k` rows and `nstrata` columns. A vector of length `k` is always
interpreted as overall targets; to specify per-stratum targets for a single
analysis, use a 1-row matrix. Overall and per-stratum event requirements can
be supplied together, and the analysis waits for all active requirements
unless a cap intervenes. A matrix passed to `targetEvents` remains accepted as
a deprecated alias, but its entries are now enforced per stratum rather than
being reduced to row sums.

`minNPerStratum` uses the same matrix layout for enrollment requirements. When
it is supplied with `minFollowUp`, the follow-up interval begins only after
every active overall and per-stratum enrollment requirement has been reached.

## Power under alternative assumptions

Passing the design's analysis times with the same hazard ratio exactly
reproduces the design power (90%). Internally, `gsSurvPower()` uses `x$n.fix`
(when `x` is provided) so that the drift parameter $\theta$ and bounds are
normalized identically to `gsSurv()`. The assumed HR's drift is obtained
by method-specific scaling:

- For Schoenfeld, LachinFoulkes, and BernsteinLagakos:
  $\theta_{\text{assumed}} = \theta_{\text{design}} \times |\log(\text{hr}/\text{hr}_0)| / |\log(\text{hr}_1/\text{hr}_0)|$
- For Freedman:
  $\theta_{\text{assumed}} = \theta_{\text{design}} \times |\delta(\text{hr})| / |\delta(\text{hr}_1)|$
  where $\delta(\text{hr}) = (\text{hr} - 1) / (\text{hr} + 1/\text{ratio})$

This reproduces the design power exactly at `hr = hr1` and scales
correctly for other hazard ratios, matching `rpact::getPowerSurvival()`
to within 0.5% across methods.

```{r gssurvpower_at_design}
cat("Design power:", round((1 - design$beta) * 100, 1), "%\n")
cat("gsSurvPower:  ", round(pwr_design$power * 100, 1), "%\n")
```

### Power under a different hazard ratio

Suppose the true treatment effect is HR = 0.8 instead of the design
assumption of 0.7:

```{r gssurvpower_worse_hr}
pwr_worse <- gsSurvPower(x = design, hr = 0.8, plannedCalendarTime = design$T)
cat("Power at HR = 0.8:", round(pwr_worse$power * 100, 1), "%\n")
```

The futility bounds remain calibrated to the design HR (0.7), but power
is evaluated under the assumed HR (0.8).

### Power over a range of hazard ratios

For instance, the following example shows how departures from the planned
hazard ratio affect power when the planned calendar analysis times are held
fixed.

```{r gssurvpower_sensitivity}
hr_grid <- seq(0.55, 0.90, by = 0.05)
power_vals <- sapply(hr_grid, function(h) {
  p <- gsSurvPower(x = design, hr = h, plannedCalendarTime = design$T)
  p$power
})
results <- data.frame(HR = hr_grid, Power = round(power_vals * 100, 1))
results
```

## Multiple timing criteria

Real trials often use multiple criteria for analysis timing. In practice,
a protocol may specify target event counts, planned calendar times,
minimum follow-up after enrollment completes, and caps on how long
analyses can be delayed. `gsSurvPower()` lets you combine all of these
in a single call. As noted above, these are expected-value calculations;
simulation is needed to characterize variability in when the rules are met.
For instance, the following examples show the impact of slower enrollment and
faster control failures when timing floors and extension caps are applied
together.

### Setup

The design enrolls at rate `r round(design$gamma, 1)` patients/month for
`r sum(design$R)` months (`r floor(sum(design$gamma * design$R))` patients total),
with a 12-month control median and HR = 0.7.
The three analyses are planned at months
`r paste(round(design$T, 1), collapse = ", ")`,
so IA1 occurs *before* planned enrollment completion and IA2/FA occur *after*.

We layer four criteria:

| Criterion | IA1 | IA2 | FA |
|-----------|-----|-----|-----|
| `plannedCalendarTime` | `r round(design$T[1],1)` | `r round(design$T[2],1)` | `r round(design$T[3],1)` |
| `targetEvents` | `r round(design$n.I[1],1)` | `r round(design$n.I[2],1)` | `r round(design$n.I[3],1)` |
| `minN + minFollowUp` | — | all enrolled + 2 mo | all enrolled + 12 mo |
| `maxExtension` | 3 mo | 12 mo | 20 mo |

The floor for each analysis is the *latest* of the applicable
`plannedCalendarTime`, `minN + minFollowUp`, and previous-analysis
constraints. If expected events have not reached the target at the
floor, the analysis can be extended until the time that expected
events reach the target, up to `plannedCalendarTime + maxExtension`.

```{r timing_setup}
total_N <- floor(sum(design$gamma * design$R))
```

### Baseline: design assumptions

Under design assumptions the criteria reproduce the planned timing
exactly. The `minN + minFollowUp` floor for IA2 is
`r sum(design$R)` + 2 = `r sum(design$R) + 2` months, which is below
the planned time of `r round(design$T[2], 1)` months, so the planned
calendar time drives IA2. The FA floor from `minN + minFollowUp`
is `r sum(design$R)` + 12 = 28, which coincides with the planned time.
Since the design's expected event counts match the targets,
the `maxExtension` is not needed and the result matches the design power.

```{r timing_baseline}
pwr_multi <- gsSurvPower(
  x = design,
  targetEvents = design$n.I,
  plannedCalendarTime = design$T,
  minN = c(NA, total_N, total_N),
  minFollowUp = c(NA, 2, 12),
  maxExtension = c(3, 12, 20)
)

data.frame(
  Analysis = 1:design$k,
  Planned_Time = round(design$T, 1),
  Actual_Time = round(pwr_multi$T, 1),
  Target_Events = round(design$n.I, 1),
  Actual_Events = round(pwr_multi$n.I, 1)
)
cat("Power:", round(pwr_multi$power * 100, 1), "%\n")
```

### Scenario 1: slower enrollment

If enrollment proceeds at half the planned rate, it takes
`r 2 * sum(design$R)` months to reach `r total_N` patients.
If we simply target the same sample size (double the duration of enrollment) 
and event targets, we will extend the expected timing of analyses from 
`r round(design$T, 1)` months but have the same power and bounds.

```{r timing_slow_enrollment_simple}
pwr_slow_simple <- gsSurvPower(
  x = design,
  gamma = design$gamma / 2,
  targetN = total_N,
  targetEvents = design$n.I
)


pwr_slow_simple |> gsBoundSummary()
```


With `maxExtension = c(3, 12, 20)`, each analysis can extend
beyond `plannedCalendarTime` by the specified amount while waiting
for expected events to reach the target—but never past the cap.
The `minN + minFollowUp` floor for IA2 is
`r 2 * sum(design$R)` + 2 = `r 2 * sum(design$R) + 2` months,
far beyond `plannedCalendarTime[2] + maxExtension[2]`, so the cap
overrides. Note that `maxExtension` is always measured from
`plannedCalendarTime`, not from the floor, so it acts as a hard
deadline on how long the sponsor is willing to wait.

```{r timing_slow_enrollment}
pwr_slow <- gsSurvPower(
  x = design,
  gamma = design$gamma / 2,
  targetEvents = design$n.I,
  plannedCalendarTime = design$T,
  minN = c(NA, total_N, total_N),
  minFollowUp = c(NA, 2, 12),
  maxExtension = c(3, 12, 20)
)

data.frame(
  Analysis = 1:design$k,
  Planned_Time = round(design$T, 1),
  Actual_Time = round(pwr_slow$T, 1),
  Target_Events = round(design$n.I, 1),
  Actual_Events = round(pwr_slow$n.I, 1)
)
cat("Power:", round(pwr_slow$power * 100, 1), "%\n")
```

Each analysis is capped by `plannedCalendarTime + maxExtension`.
The final analysis achieves
only `r round(pwr_slow$n.I[design$k], 0)` of the targeted
`r round(design$n.I[design$k], 0)` events, and power drops from 90%
to `r round(pwr_slow$power * 100, 1)`%.

### Scenario 2: higher control failure rate

If the control median is 8 months instead of 12, events accumulate
faster. Expected events reach the target well before the planned
calendar times, so the `plannedCalendarTime` floor determines the
analysis schedule. The trial over-runs its event targets
substantially, yielding higher-than-planned power.

```{r timing_fast_failure}
pwr_fast <- gsSurvPower(
  x = design,
  lambdaC = log(2) / 8,
  targetEvents = design$n.I,
  plannedCalendarTime = design$T,
  minN = c(NA, total_N, total_N),
  minFollowUp = c(NA, 2, 12),
  maxExtension = c(3, 12, 20)
)

data.frame(
  Analysis = 1:design$k,
  Planned_Time = round(design$T, 1),
  Actual_Time = round(pwr_fast$T, 1),
  Target_Events = round(design$n.I, 1),
  Actual_Events = round(pwr_fast$n.I, 1)
)
cat("Power:", round(pwr_fast$power * 100, 1), "%\n")
```

The final analysis collects `r round(pwr_fast$n.I[design$k], 0)` events
vs. the target of `r round(design$n.I[design$k], 0)`, and power rises to
`r round(pwr_fast$power * 100, 1)`%.

### Controlling effective spending time

When events fall short of the target (as in Scenario 1), the actual
event count at each analysis may be lower than planned. By default, bounds are
computed at the scenario's information fractions, `n.I / max(n.I)`, which
always end at 1. With a reference design `x`,
`spending = "min_planned_actual"` instead uses
`pmin(x$n.I, scenario n.I) / x$n.I[k]`. This is the expected-value analogue of
simtrial's planned-versus-actual spending option: spending cannot run ahead of
the planned event count at a look or the events expected under the scenario.

```{r min_planned_actual_spending}
pwr_slow_min_spending <- gsSurvPower(
  x = design,
  gamma = design$gamma / 2,
  targetEvents = design$n.I,
  plannedCalendarTime = design$T,
  minN = c(NA, total_N, total_N),
  minFollowUp = c(NA, 2, 12),
  maxExtension = c(3, 12, 20),
  spending = "min_planned_actual"
)

data.frame(
  Analysis = 1:design$k,
  Planned_Events = round(design$n.I, 1),
  Scenario_Events = round(pwr_slow_min_spending$n.I, 1),
  Effective_Spending_Time = round(
    pmin(design$n.I, pwr_slow_min_spending$n.I) / design$n.I[design$k],
    3
  )
)
```

The same effective spending-time vector is used for the upper and lower
bounds. Set `fullSpendingAtFinal = TRUE` to spend fully at the final analysis,
corresponding to simtrial's `fa_alpha_spending = "full_alpha"`; leave it
`FALSE` for information-fraction final spending.

#### User-specified spending-time caps with `informationRates`

For a spending schedule specified directly rather than through the reference
design's planned event counts, `informationRates` supplies planned
information-fraction caps.
The effective spending time is
`pmin(informationRates, actual_timing)` at each analysis. This prevents
spending ahead of the planned information schedule when events arrive faster
than planned and ahead of the information actually accumulated when events
arrive slower. If `informationRates` is supplied, it takes precedence over
`spending = "calendar"` and over manual `usTime` / `lsTime` overrides.

Setting `fullSpendingAtFinal = TRUE` forces both the upper and lower effective
spending times at the final analysis to 1 after the caps are applied. The
example below uses a final planned information-fraction cap of
0.95 to show the effect explicitly. Without `fullSpendingAtFinal`,
the final effective spending time would remain 0.95 rather than 1.

```{r informationRates_demo}
# Scenario 1 with informationRates and fullSpendingAtFinal
planned_info_rates <- c(design$timing[-design$k], 0.95)

pwr_slow_ir <- gsSurvPower(
  x = design,
  gamma = design$gamma / 2,
  targetEvents = design$n.I,
  plannedCalendarTime = design$T,
  minN = c(NA, total_N, total_N),
  minFollowUp = c(NA, 2, 12),
  maxExtension = c(3, 12, 20),
  informationRates = planned_info_rates,
  fullSpendingAtFinal = TRUE
)

effective_spending_time <- pmin(planned_info_rates, pwr_slow_ir$timing)
effective_spending_time[design$k] <- 1

data.frame(
  Analysis = 1:design$k,
  Actual_Events = round(pwr_slow_ir$n.I, 1),
  Actual_InfoFrac = round(pwr_slow_ir$timing, 3),
  Planned_InfoFrac = round(planned_info_rates, 3),
  Effective_Spending_Time = round(effective_spending_time, 3),
  Futility_Bound = round(pwr_slow_ir$lower$bound, 3),
  Incremental_Beta_Spend = round(pwr_slow_ir$lower$spend, 4)
)
cat("Power (default spending):       ", round(pwr_slow$power * 100, 1), "%\n")
cat("Power (capped + full final):    ", round(pwr_slow_ir$power * 100, 1), "%\n")
```

With `fullSpendingAtFinal = TRUE`, the final upper and lower effective spending
times are 1 even though the planned-vs-actual cap would otherwise be
0.95. The displayed incremental beta spending shows how design beta is
allocated across the recalibrated futility bounds. This produces slightly
different final bounds compared to the same `informationRates` specification
with `fullSpendingAtFinal = FALSE`.

## Additional and advanced examples

The remaining sections cover validation against standard power plots and more
specialized changes to spending, alpha, and stratified assumptions. They are
useful when the basic scenario-and-timing workflow above is not sufficient.
For instance, in the following example the impact of fixing event counts
rather than calendar times is shown across a range of hazard ratios.

### Comparison with gsDesign power plots

The `gsDesign` package provides power plots via `plot(design, plottype = 2)`.
These hold event counts fixed at the design values and vary only the drift
parameter $\theta$. The table below compares three approaches across a range
of hazard ratios:

- **gsDesign**: `gsProbability()` with design bounds, design events,
  scaled $\theta$. This is what `plot(design, plottype = 2)` computes.
- **gsSurvPower (fixed events)**: `gsSurvPower()` with
  `targetEvents = design_events`. Events are held constant; calendar
  times adjust.
- **gsSurvPower (fixed calendar)**: `gsSurvPower()` with
  `plannedCalendarTime = design$T`. Calendar times are held constant;
  events change with the assumed HR.

```{r gssurvpower_vs_gsdesign}
design_events <- design$n.I
hr_grid <- seq(0.55, 0.95, by = 0.05)

comparison <- data.frame(
  HR = hr_grid,
  gsDesign_plot = sapply(hr_grid, function(h) {
    delta_ratio <- abs(log(h)) / abs(log(design$hr))
    theta_h <- design$delta * delta_ratio
    gsp <- gsDesign::gsProbability(
      k = design$k, theta = theta_h,
      n.I = design$n.I,
      a = design$lower$bound, b = design$upper$bound, r = 18)
    sum(gsp$upper$prob)
  }),
  fixed_events = sapply(hr_grid, function(h) {
    gsSurvPower(x = design, hr = h, targetEvents = design_events)$power
  }),
  fixed_calendar = sapply(hr_grid, function(h) {
    gsSurvPower(x = design, hr = h, plannedCalendarTime = design$T)$power
  })
)
comparison[, -1] <- round(comparison[, -1] * 100, 2)
comparison
```

**Key observations:**

- The `gsDesign_plot` and `fixed_events` columns match to numerical
  precision because both condition on the same event counts at each
  analysis. When using `targetEvents`, `gsSurvPower()` reproduces the
  `gsDesign` power plot exactly.

- The `fixed_calendar` column differs modestly because fixing calendar
  times allows the expected event count to change with the assumed HR.
  A worse HR (closer to 1) produces more expected events at the same
  calendar time, since the experimental arm has a higher failure rate.
  This slightly changes the statistical information at each analysis,
  producing an "unconditional" power that accounts for the interplay
  between treatment effect and event accrual.

#### Bounds stability

When using `targetEvents`, the efficacy and futility bounds do not change
with the assumed HR. The bounds are determined entirely by the design
parameters (alpha/beta spending, information fractions, `n.fix`) and are
reused directly from the input design `x` when the timing matches:

```{r gssurvpower_bounds}
design_events <- design$n.I
cat("Design bounds (Z-scale):\n")
cat("  Efficacy:", round(design$upper$bound, 4), "\n")
cat("  Futility:", round(design$lower$bound, 4), "\n\n")

for (h in c(0.5, 0.7, 0.8, 1.0)) {
  pwr <- gsSurvPower(x = design, hr = h, targetEvents = design_events)
  cat(sprintf("HR=%.1f  Efficacy: %s  Futility: %s  (identical: %s)\n",
    h,
    paste(round(pwr$upper$bound, 4), collapse = ", "),
    paste(round(pwr$lower$bound, 4), collapse = ", "),
    identical(pwr$upper$bound, design$upper$bound) &&
      identical(pwr$lower$bound, design$lower$bound)))
}
```

With `plannedCalendarTime`, different assumed HRs produce different expected
event counts and therefore different information fractions, so the bounds
are appropriately recomputed via `gsDesign::gsDesign()`.

### Changing alpha

A common use case is evaluating power at a different one-sided alpha level —
for example, when a graphical multiplicity procedure initially allocates
$\alpha = 0.0125$ to one hypothesis and later, after another hypothesis is
rejected, propagates alpha so that $\alpha = 0.025$ is available.

Here we design at $\alpha = 0.0125$ and then ask: what is the power if
we can test at $\alpha = 0.025$?

When `x` is provided and the information fractions (timing) match the
original design, `gsSurvPower()` recalculates **efficacy bounds** at the
new alpha using the non-binding efficacy convention while **preserving the
original efficacy testing schedule and futility bounds** from `x`. This
follows the same convention as `gsBoundSummary()`. Any futility bound that
would exceed the new efficacy bound is clipped.

For test types 1, 4, 6, and 8 this non-binding calculation is appropriate for
evaluating alpha propagated by a Maurer--Bretz graphical multiple-testing
procedure. For binding test types 2, 3, 5, and 7, the calculation below is a
planning sensitivity analysis only and should not be interpreted as graphical
alpha recycling or as a sequential p-value.

When timing changes (e.g., different `targetEvents`), both bounds are
recomputed from scratch using the full `test.type` and spending functions.

```{r alpha_change}
# Design at one-sided alpha = 0.0125
design_a0125 <- gsSurv(
  k = 3, test.type = 4, alpha = 0.0125, sided = 1, beta = 0.1,
  sfu = sfHSD, sfupar = -4, sfl = sfHSD, sflpar = -2,
  lambdaC = log(2) / 12, hr = 0.7, hr0 = 1,
  eta = 0.01, gamma = 10, R = 16, minfup = 12, T = 28
)

cat("=== Original design (alpha = 0.0125) ===\n")
cat("Efficacy bounds:", round(design_a0125$upper$bound, 4), "\n")
cat("Futility bounds:", round(design_a0125$lower$bound, 4), "\n\n")

# Power at alpha = 0.025 with same event counts (timing preserved)
events_a0125 <- design_a0125$n.I
pwr_a025 <- gsSurvPower(x = design_a0125, alpha = 0.025, targetEvents = events_a0125)

cat("=== gsSurvPower at alpha = 0.025 ===\n")
cat("Efficacy bounds:", round(pwr_a025$upper$bound, 4), "\n")
cat("Futility bounds:", round(pwr_a025$lower$bound, 4), "\n")
cat("Power:          ", round(pwr_a025$power * 100, 1), "%\n\n")

# Cross-check: gsBoundSummary at the same alternate alpha
# (only non-binding test.type 1, 4, 6, and 8 are supported)
cat("=== gsBoundSummary (alpha = 0.025) ===\n")
print(gsBoundSummary(design_a0125, alpha = 0.025))
```

Note that `gsBoundSummary()` adds an $\alpha = 0.025$ column for
non-binding `test.type` 1, 4, 6, and 8. It does not support alternate alpha
for binding types 2, 3, 5, or 7. `gsSurvPower()` can still recompute efficacy
with the non-binding convention for a binding input design, retain the original
`testUpper` schedule, and keep the original futility bounds, but that result is
a planning sensitivity analysis rather than a Maurer--Bretz multiplicity
calculation.

#### Binding type planning sensitivity example (test.type = 3)

```{r alpha_binding}
design3 <- gsSurv(
  k = 3, test.type = 3, alpha = 0.0125, sided = 1, beta = 0.1,
  sfu = sfHSD, sfupar = -4, sfl = sfHSD, sflpar = -2,
  lambdaC = log(2) / 12, hr = 0.7, eta = 0.01,
  gamma = 10, R = 16, minfup = 12, T = 28
)
events3 <- design3$n.I

pwr3_a025 <- gsSurvPower(x = design3, alpha = 0.025, targetEvents = events3)

cat("=== Binding futility (test.type=3) at alpha = 0.025 ===\n")
cat("Original efficacy:", round(design3$upper$bound, 4), "\n")
cat("New efficacy:     ", round(pwr3_a025$upper$bound, 4), "\n")
cat("Original futility:", round(design3$lower$bound, 4), "\n")
cat("New futility:     ", round(pwr3_a025$lower$bound, 4), "\n")
cat("Power:            ", round(pwr3_a025$power * 100, 1), "%\n")
```

The futility bounds are preserved from the original design. At the final
analysis where the original futility bound equals the original efficacy
bound, the new efficacy bound (which is lower) becomes the clip point.

The output retains the original `test.type` and records the new `alpha`:

```{r alpha_output_fields}
cat("test.type:", pwr3_a025$test.type, "(same as input design)\n")
cat("alpha:    ", pwr3_a025$alpha, "(updated to new value)\n")
```

### Event-based timing

Instead of calendar times, analyses can be triggered by target event counts.
For instance, the following example fixes the information at each analysis
through event targets while allowing the expected calendar times to adjust.

```{r gssurvpower_events}
pwr_events <- gsSurvPower(
  x = design,
  targetEvents = c(75, 150, 225)
)
cat("Analysis times:", round(pwr_events$T, 1), "\n")
cat("Events at each analysis:",
    round(pwr_events$n.I, 1), "\n")
cat("Power:", round(pwr_events$power * 100, 1), "%\n")
```

### Slower enrollment at fixed analysis times

Another common sensitivity analysis is slower-than-planned enrollment. When
calendar analysis times are fixed, slower enrollment reduces the expected number
of events available at each look and therefore reduces power.

```{r gssurvpower_slower_enrollment}
pwr_slow_enroll <- gsSurvPower(
  x = design,
  gamma = design$gamma / 2,
  plannedCalendarTime = design$T
)

cat("Original final expected events:", round(pwr_design$n.I[design$k], 1), "\n")
cat("Slower-enrollment final events:", round(pwr_slow_enroll$n.I[design$k], 1), "\n")
cat("Original power:", round(pwr_design$power * 100, 1), "%\n")
cat("Slower-enrollment power:", round(pwr_slow_enroll$power * 100, 1), "%\n")
```

### Calendar-based spending

By default, alpha and beta spending track statistical **information fractions**
(`n.I / max(n.I)`). Setting `spending = "calendar"` instead ties spending to
calendar time fractions (`T / max(T)`).
This distinction matters when analysis times are unevenly spaced:
event accrual is slow early in the trial (enrollment is ongoing),
so by the time one-third of the statistical information has accumulated
the trial is already well past one-third of its calendar duration.
The first analysis in this design occurs about 12.4 months into a
28-month trial—an information fraction of 0.333 but a calendar fraction
of 0.444. Calendar spending therefore spends more alpha and beta early,
producing less conservative interim efficacy and futility bounds and slightly
more conservative final bounds.

When `spending = "calendar"`, any user-supplied `usTime` and `lsTime`
overrides are **ignored**; the realized analysis times determine spending
fractions automatically.

```{r calendar_spending}
# Information-based spending (default)
pwr_info <- gsSurvPower(
  x = design,
  plannedCalendarTime = design$T,
  spending = "information"
)

# Calendar-based spending
pwr_cal <- gsSurvPower(
  x = design,
  plannedCalendarTime = design$T,
  spending = "calendar"
)

# Compare spending fractions and bounds
data.frame(
  Analysis = 1:design$k,
  Calendar_Time = round(pwr_info$T, 1),
  InfoFraction = round(pwr_info$timing, 3),
  CalendarFraction = round(pwr_cal$T / max(pwr_cal$T), 3),
  Efficacy_Info = round(pwr_info$upper$bound, 4),
  Efficacy_Calendar = round(pwr_cal$upper$bound, 4),
  Futility_Info = round(pwr_info$lower$bound, 4),
  Futility_Calendar = round(pwr_cal$lower$bound, 4),
  Beta_Spend_Info = round(pwr_info$lower$spend, 4),
  Beta_Spend_Calendar = round(pwr_cal$lower$spend, 4)
)
```

At analysis 1 (month ~12), the calendar fraction (0.444) substantially
exceeds the information fraction (0.333). Calendar spending allocates
more alpha to this look, so the efficacy bound drops from 3.01 to 2.84—
a meaningful difference for interim decision-making. By the final
analysis the bounds nearly converge because both fractions equal 1.
The same change allocates more design beta to the first look and raises the
futility bound from -0.24 to -0.06, making early futility stopping easier.

Note that passing `usTime` or `lsTime` with `spending = "calendar"`
has no effect—the calendar fractions override them:

```{r calendar_ignores_usTime}
pwr_cal_override <- gsSurvPower(
  x = design,
  plannedCalendarTime = design$T,
  spending = "calendar",
  usTime = c(0.2, 0.6, 1),
  lsTime = c(0.3, 0.8, 1)
)

# Bounds are identical regardless of usTime/lsTime
identical(pwr_cal$upper$bound, pwr_cal_override$upper$bound)
```

### Stratified event targets

When a trial enrolls patients from multiple strata with different event
rates, you may want to specify per-stratum event targets rather than a
single overall number. `targetEventsPerStratum` accepts a matrix with `k` rows
(analyses) and `nstrata` columns (strata). The analysis waits until each
non-`NA` entry in its row is reached. Because event rates can differ across
strata, events in one stratum can exceed its requirement while the other
stratum catches up; the overall count therefore need not equal the row sum.

Consider a two-stratum design where stratum 1 has median survival of
6 months and stratum 2 has 12 months. We require at least 20 and 10 events by
stratum at the interim, and 40 and 20 events by stratum at the final analysis:

```{r stratified_events}
# Per-stratum event targets: rows = analyses, columns = strata
event_matrix <- matrix(
  c(20, 10,   # interim: 20 from stratum 1, 10 from stratum 2
    40, 20),  # final:   40 from stratum 1, 20 from stratum 2
  nrow = 2, byrow = TRUE
)

pwr_strat <- gsSurvPower(
  k = 2, test.type = 1, alpha = 0.025, sided = 1,
  lambdaC = matrix(log(2) / c(6, 12), ncol = 2),
  hr = 0.7, eta = 0.01,
  gamma = matrix(c(5, 5), ncol = 2), R = 12, ratio = 1,
  targetEventsPerStratum = event_matrix
)

# Every stratum requirement is met; one can be exceeded while another catches up.
actual_events_by_stratum <- pwr_strat$eDC + pwr_strat$eDE
data.frame(
  Analysis = 1:2,
  Target_Stratum1 = event_matrix[, 1],
  Expected_Stratum1 = round(actual_events_by_stratum[, 1], 1),
  Target_Stratum2 = event_matrix[, 2],
  Expected_Stratum2 = round(actual_events_by_stratum[, 2], 1),
  Expected_Total = round(pwr_strat$n.I, 1),
  Calendar_Time = round(pwr_strat$T, 1)
)

cat("Power:", round(pwr_strat$power * 100, 1), "%\n")
```

The biomarker example below also uses matrix-valued `lambdaC` and `gamma` to
represent strata, but its timing is driven by `plannedCalendarTime` rather
than per-stratum targets.

### Biomarker subgroup to stratified design

A common scenario is designing a trial for a biomarker-defined subgroup,
then assessing what power the same enrollment provides for the overall
(stratified) population under a more conservative treatment effect.
Note that the `gsDesign2` package could be used to design with different
hazard ratios in the biomarker-positive and biomarker-negative populations
simultaneously; here we illustrate the simpler approach using `gsSurvPower()`.

#### Step 1: Design for the biomarker-positive subgroup

Suppose 60% of the population is biomarker-positive (prevalence = 0.6),
the control median survival in this subgroup is 12 months,
the hazard ratio is 0.65, and we target 90% power at one-sided
$\alpha = 0.0125$ (e.g., from a graphical multiplicity allocation):

```{r biomarker_design}
prevalence <- 0.6
median_bm_pos <- 12    # control median in biomarker+ (months)
median_bm_neg <- 10    # control median in biomarker- (shorter prognosis)

bm_design <- gsSurvCalendar(
  test.type = 4, alpha = 0.0125, beta = 0.1,
  sfu = sfHSD, sfupar = -4, sfl = sfHSD, sflpar = -2,
  calendarTime = c(12, 24, 36),
  lambdaC = log(2) / median_bm_pos, hr = 0.65, eta = 0.01,
  gamma = 10, R = 18, minfup = 18, ratio = 1
)

summary(bm_design)
gsBoundSummary(bm_design)
```

#### Step 2: Power for the overall (stratified) population

Now consider enrolling the entire population using the same enrollment
duration and analysis calendar times. The biomarker-positive enrollment
rate comes from the subgroup design. The biomarker-negative enrollment
rate is proportionate based on prevalence: if biomarker-positive patients
enroll at rate $\gamma_{+}$, biomarker-negative patients enroll at rate
$\gamma_{-} = \gamma_{+} \times (1 - p) / p$.

We use a **stratified** approach: `lambdaC` and `gamma` are specified as
matrices with two columns (one per stratum), allowing different control
hazard rates and enrollment rates in each biomarker subgroup.
The overall hazard ratio assumed is 0.75 (attenuated because the
biomarker-negative subgroup has a weaker treatment effect).

```{r stratified_power}
# Control hazard rates by stratum
lambdaC_pos <- log(2) / median_bm_pos
lambdaC_neg <- log(2) / median_bm_neg

# Enrollment rates by stratum (proportionate to prevalence)
gamma_pos <- bm_design$gamma
gamma_neg <- bm_design$gamma * (1 - prevalence) / prevalence

# Stratified inputs: matrix with columns = strata
lambdaC_strat <- matrix(c(lambdaC_pos, lambdaC_neg), ncol = 2)
gamma_strat <- matrix(c(gamma_pos, gamma_neg), ncol = 2)

pwr_overall <- gsSurvPower(
  k = 3, test.type = 4, alpha = 0.0125, sided = 1,
  sfu = sfHSD, sfupar = -4, sfl = sfHSD, sflpar = -2,
  lambdaC = lambdaC_strat, hr = 0.75, eta = 0.01,
  gamma = gamma_strat, R = 18, ratio = 1,
  plannedCalendarTime = c(12, 24, 36)
)

summary(pwr_overall)
gsBoundSummary(pwr_overall)
```

The overall design enrolls more patients (the full population rather than
just the 60% biomarker-positive subgroup) and has a higher event rate
in the biomarker-negative stratum (shorter control median), but assumes a
weaker overall treatment effect (HR = 0.75 vs. 0.65).
