diff --git a/DESCRIPTION b/DESCRIPTION
index d86616e7..2ad68f4a 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -46,6 +46,7 @@ Imports:
npsurvSS,
r2rtf,
stats,
+ survival,
tibble,
tidyr,
utils,
@@ -56,6 +57,7 @@ Suggests:
kableExtra,
knitr,
rmarkdown,
+ simtrial,
testthat (>= 3.0.0)
VignetteBuilder:
knitr
diff --git a/NAMESPACE b/NAMESPACE
index 8a5a39ab..eee1a1c9 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -9,6 +9,7 @@ S3method(summary,gs_design)
S3method(to_integer,fixed_design)
S3method(to_integer,gs_design)
export(ahr)
+export(ahr_blinded)
export(as_gt)
export(as_rtf)
export(define_enroll_rate)
@@ -81,6 +82,7 @@ importFrom(stats,pnorm)
importFrom(stats,qnorm)
importFrom(stats,stepfun)
importFrom(stats,uniroot)
+importFrom(survival,Surv)
importFrom(tibble,tibble)
importFrom(utils,tail)
useDynLib(gsDesign2, .registration = TRUE)
diff --git a/R/ahr_blinded.R b/R/ahr_blinded.R
new file mode 100644
index 00000000..11ba4dbe
--- /dev/null
+++ b/R/ahr_blinded.R
@@ -0,0 +1,123 @@
+# Copyright (c) 2024 Merck & Co., Inc., Rahway, NJ, USA and its affiliates.
+# All rights reserved.
+#
+# This file is part of the gsDesign2 program.
+#
+# gsDesign2 is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+#' Blinded estimation of average hazard ratio
+#'
+#' Based on blinded data and assumed hazard ratios in different intervals,
+#' compute a blinded estimate of average hazard ratio (AHR) and corresponding
+#' estimate of statistical information.
+#' This function is intended for use in computing futility bounds based on
+#' spending assuming the input hazard ratio (hr) values for intervals
+#' specified here.
+#'
+#' @param surv Input survival object (see [survival::Surv()]);
+#' note that only 0 = censored, 1 = event for [survival::Surv()].
+#' @param intervals Vector containing positive values indicating
+#' interval lengths where the exponential rates are assumed.
+#' Note that a final infinite interval is added if any events occur
+#' after the final interval specified.
+#' @param hr Vector of hazard ratios assumed for each interval.
+#' @param ratio Ratio of experimental to control randomization.
+#'
+#' @return A `tibble` with one row containing
+#' - `ahr` - Blinded average hazard ratio based on assumed period-specific
+#' hazard ratios input in `fail_rate` and observed events in the
+#' corresponding intervals.
+#' - `event` - Total observed number of events.
+#' - `info0` - Information under related null hypothesis.
+#' - `theta` - Natural parameter for group sequential design representing
+#' expected incremental drift at all analyses.
+#'
+#' @section Specification:
+#' \if{latex}{
+#' \itemize{
+#' \item Validate if input hr is a numeric vector.
+#' \item Validate if input hr is non-negative.
+#' \item Simulate piece-wise exponential survival estimation with the inputs
+#' survival object surv and intervals.
+#' \item Save the length of hr and events to an object, and if the length of
+#' hr is shorter than the intervals, add replicates of the last element of hr
+#' and the corresponding numbers of events to hr.
+#' \item Compute the blinded estimation of average hazard ratio.
+#' \item Compute adjustment for information.
+#' \item Return a tibble of the sum of events, average hazard ratio,
+#' blinded average hazard ratio, and the information.
+#' }
+#' }
+#' \if{html}{The contents of this section are shown in PDF user manual only.}
+#'
+#' @importFrom tibble tibble
+#' @importFrom survival Surv
+#'
+#' @export
+#'
+#' @examples
+#' ahr_blinded(
+#' surv = survival::Surv(
+#' time = simtrial::ex2_delayed_effect$month,
+#' event = simtrial::ex2_delayed_effect$evntd
+#' ),
+#' intervals = c(4, 100),
+#' hr = c(1, .55),
+#' ratio = 1
+#' )
+ahr_blinded <- function(
+ surv = survival::Surv(
+ time = simtrial::ex1_delayed_effect$month,
+ event = simtrial::ex1_delayed_effect$evntd
+ ),
+ intervals = c(3, Inf),
+ hr = c(1, .6),
+ ratio = 1) {
+ # Input checking
+ if (!is.vector(hr, mode = "numeric") || min(hr) <= 0) {
+ stop("ahr_blinded: hr must be a vector of positive numbers.")
+ }
+
+ tte_data <- data.frame(time = surv[, "time"], status = surv[, "status"])
+ if (nrow(subset(tte_data, time > sum(intervals) & status > 0)) > 0) {
+ intervals_imputed <- c(intervals, Inf)
+ } else {
+ intervals_imputed <- intervals
+ }
+ if (length(intervals_imputed) != length(hr)) {
+ stop("ahr_blinded: the piecewise model specified hr and intervals are not aligned.")
+ }
+
+ # Fit the survival data into piecewise exponential model
+ event <- simtrial::fit_pwexp(surv, intervals)[, 3]
+ nhr <- length(hr)
+ nx <- length(event)
+
+ # Add to hr if length shorter than intervals
+ if (length(hr) < length(event)) hr <- c(hr, rep(hr[nhr], nx - nhr))
+
+ # Compute blinded AHR
+ theta <- -sum(log(hr[1:nx]) * event) / sum(event)
+
+ # Compute adjustment for information
+ q_e <- ratio / (1 + ratio)
+
+ ans <- tibble(
+ event = sum(event),
+ ahr = exp(-theta),
+ theta = theta,
+ info0 = sum(event) * (1 - q_e) * q_e
+ )
+ return(ans)
+}
diff --git a/R/globals.R b/R/globals.R
index 0e1e6b44..30fb57a3 100644
--- a/R/globals.R
+++ b/R/globals.R
@@ -23,6 +23,8 @@ utils::globalVariables(
".", ".SD",
# From `ahr()`
c("stratum", "rate", "hr", "treatment", "time", "info0", "info"),
+ # From `ahr_blinded()`
+ c("status"),
# From `as_gt.gs_design()`
c("Bound", "Alternate hypothesis", "Null hypothesis", "Analysis"),
# From `expected_accrual()`
diff --git a/R/gs_info_wlr.R b/R/gs_info_wlr.R
index 776c4f2f..89c1b7a5 100644
--- a/R/gs_info_wlr.R
+++ b/R/gs_info_wlr.R
@@ -1,9 +1,9 @@
# Copyright (c) 2024 Merck & Co., Inc., Rahway, NJ, USA and its affiliates.
# All rights reserved.
#
-# This file is part of the gsdmvn program.
+# This file is part of the gsDesign2 program.
#
-# gsdmvn is free software: you can redistribute it and/or modify
+# gsDesign2 is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
diff --git a/_pkgdown.yml b/_pkgdown.yml
index 5f34842f..220c4af0 100644
--- a/_pkgdown.yml
+++ b/_pkgdown.yml
@@ -46,6 +46,7 @@ reference:
- gs_info_ahr
- gs_power_ahr
- gs_design_ahr
+ - ahr_blinded
- title: "Weighted logrank"
desc: >
Functions for the weighted logrank test (WLR) method.
@@ -165,6 +166,7 @@ articles:
- articles/story-ahr-under-nph
- articles/story-integer-design
- articles/story-spending-time-example
+ - articles/story-update-boundary
- title: "Designs with binary endpoints"
desc: >
diff --git a/inst/helper_function/ahr_blinded.R b/inst/helper_function/ahr_blinded.R
deleted file mode 100644
index 0473f0ef..00000000
--- a/inst/helper_function/ahr_blinded.R
+++ /dev/null
@@ -1,102 +0,0 @@
-# This file is part of the gsDesign2 program.
-#
-# gsDesign2 is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-#' Blinded estimation of average hazard ratio
-#'
-#' Based on blinded data and assumed hazard ratios in different intervals, compute
-#' a blinded estimate of average hazard ratio (AHR) and corresponding estimate of statistical information.
-#' This function is intended for use in computing futility bounds based on spending assuming
-#' the input hazard ratio (hr) values for intervals specified here.
-#' @importFrom tibble tibble
-#' @importFrom survival Surv
-#' @param Srv input survival object (see \code{Surv}); note that only 0=censored, 1=event for \code{Surv}
-#' @param intervals Vector containing positive values indicating interval lengths where the
-#' exponential rates are assumed.
-#' Note that a final infinite interval is added if any events occur after the final interval
-#' specified.
-#' @param hr vector of hazard ratios assumed for each interval
-#' @param ratio ratio of experimental to control randomization.
-#'
-#' @section Specification:
-#' \if{latex}{
-#' \itemize{
-#' \item Validate if input hr is a numeric vector.
-#' \item Validate if input hr is non-negative.
-#' \item Simulate piece-wise exponential survival estimation with the inputs survival object Srv
-#' and intervals.
-#' \item Save the length of hr and events to an object, and if the length of hr is shorter than
-#' the intervals, add replicates of the last element of hr and the corresponding numbers of events
-#' to hr.
-#' \item Compute the blinded estimation of average hazard ratio.
-#' \item Compute adjustment for information.
-#' \item Return a tibble of the sum of events, average hazard raito, blinded average hazard
-#' ratio, and the information.
-#' }
-#' }
-#' \if{html}{The contents of this section are shown in PDF user manual only.}
-#'
-#' @return A \code{tibble} with one row containing
-#' `AHR` blinded average hazard ratio based on assumed period-specific hazard ratios input in `failRates`
-#' and observed events in the corresponding intervals
-#' `Events` total observed number of events, `info` statistical information based on Schoenfeld approximation,
-#' and info0 (information under related null hypothesis) for each value of `totalDuration` input;
-#' if `simple=FALSE`, `Stratum` and `t` (beginning of each constant HR period) are also returned
-#' and `HR` is returned instead of `AHR`
-#'
-#' @examples
-#' \donttest{
-#' library(simtrial)
-#' library(survival)
-#' ahr_blinded(
-#' Srv = Surv(
-#' time = simtrial::Ex2delayedEffect$month,
-#' event = simtrial::Ex2delayedEffect$evntd
-#' ),
-#' intervals = c(4, 100),
-#' hr = c(1, .55),
-#' ratio = 1
-#' )
-#' }
-#'
-#' @export
-ahr_blinded <- function(Srv = Surv(
- time = simtrial::Ex1delayedEffect$month,
- event = simtrial::Ex1delayedEffect$evntd
- ),
- intervals = array(3, 3),
- hr = c(1, .6),
- ratio = 1) {
- msg <- "hr must be a vector of positive numbers"
- if (!is.vector(hr, mode = "numeric")) stop(msg)
- if (min(hr) <= 0) stop(msg)
-
- events <- simtrial::pwexpfit(Srv, intervals)[, 3]
- nhr <- length(hr)
- nx <- length(events)
- # Add to hr if length shorter than intervals
- if (length(hr) < length(events)) hr <- c(hr, rep(hr[nhr], nx - nhr))
-
- # Compute blinded AHR
- theta <- sum(log(hr[1:nx]) * events) / sum(events)
-
- # Compute adjustment for information
- Qe <- ratio / (1 + ratio)
-
- ans <- tibble(
- Events = sum(events), AHR = exp(theta),
- theta = theta, info0 = sum(events) * (1 - Qe) * Qe
- )
- return(ans)
-}
diff --git a/man/ahr_blinded.Rd b/man/ahr_blinded.Rd
new file mode 100644
index 00000000..77391653
--- /dev/null
+++ b/man/ahr_blinded.Rd
@@ -0,0 +1,78 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ahr_blinded.R
+\name{ahr_blinded}
+\alias{ahr_blinded}
+\title{Blinded estimation of average hazard ratio}
+\usage{
+ahr_blinded(
+ surv = survival::Surv(time = simtrial::ex1_delayed_effect$month, event =
+ simtrial::ex1_delayed_effect$evntd),
+ intervals = c(3, Inf),
+ hr = c(1, 0.6),
+ ratio = 1
+)
+}
+\arguments{
+\item{surv}{Input survival object (see \code{\link[survival:Surv]{survival::Surv()}});
+note that only 0 = censored, 1 = event for \code{\link[survival:Surv]{survival::Surv()}}.}
+
+\item{intervals}{Vector containing positive values indicating
+interval lengths where the exponential rates are assumed.
+Note that a final infinite interval is added if any events occur
+after the final interval specified.}
+
+\item{hr}{Vector of hazard ratios assumed for each interval.}
+
+\item{ratio}{Ratio of experimental to control randomization.}
+}
+\value{
+A \code{tibble} with one row containing
+\itemize{
+\item \code{ahr} - Blinded average hazard ratio based on assumed period-specific
+hazard ratios input in \code{fail_rate} and observed events in the
+corresponding intervals.
+\item \code{event} - Total observed number of events.
+\item \code{info0} - Information under related null hypothesis.
+\item \code{theta} - Natural parameter for group sequential design representing
+expected incremental drift at all analyses.
+}
+}
+\description{
+Based on blinded data and assumed hazard ratios in different intervals,
+compute a blinded estimate of average hazard ratio (AHR) and corresponding
+estimate of statistical information.
+This function is intended for use in computing futility bounds based on
+spending assuming the input hazard ratio (hr) values for intervals
+specified here.
+}
+\section{Specification}{
+
+\if{latex}{
+ \itemize{
+ \item Validate if input hr is a numeric vector.
+ \item Validate if input hr is non-negative.
+ \item Simulate piece-wise exponential survival estimation with the inputs
+ survival object surv and intervals.
+ \item Save the length of hr and events to an object, and if the length of
+ hr is shorter than the intervals, add replicates of the last element of hr
+ and the corresponding numbers of events to hr.
+ \item Compute the blinded estimation of average hazard ratio.
+ \item Compute adjustment for information.
+ \item Return a tibble of the sum of events, average hazard ratio,
+ blinded average hazard ratio, and the information.
+ }
+}
+\if{html}{The contents of this section are shown in PDF user manual only.}
+}
+
+\examples{
+ahr_blinded(
+ surv = survival::Surv(
+ time = simtrial::ex2_delayed_effect$month,
+ event = simtrial::ex2_delayed_effect$evntd
+ ),
+ intervals = c(4, 100),
+ hr = c(1, .55),
+ ratio = 1
+)
+}
diff --git a/vignettes/articles/story-update-boundary.Rmd b/vignettes/articles/story-update-boundary.Rmd
new file mode 100644
index 00000000..cda953af
--- /dev/null
+++ b/vignettes/articles/story-update-boundary.Rmd
@@ -0,0 +1,556 @@
+---
+title: "Efficacy and futility boundary update"
+author: "Yujie Zhao and Keaven M. Anderson"
+output:
+ rmarkdown::html_document:
+ toc: true
+ toc_float: true
+ toc_depth: 2
+ number_sections: true
+ highlight: "textmate"
+ css: "custom.css"
+bibliography: "gsDesign2.bib"
+vignette: >
+ %\VignetteIndexEntry{Efficacy and futility boundary update}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r setup, include=FALSE}
+knitr::opts_chunk$set(echo = FALSE)
+```
+
+```{r, message=FALSE, warning=FALSE}
+library(gsDesign2)
+```
+
+# Design assumptions
+
+We assume two analyses: an interim analysis (IA) and a final analysis (FA).
+The IA is planned 20 months after opening enrollment, followed by the FA at
+month 36.
+The planned enrollment period spans 14 months, with the first 2 months having
+an enrollment rate of 1/3 the final rate, the next 2 months with a rate of 2/3
+of the final rate, and the final rate for the remaining 10 months.
+To obtain the targeted 90\% power, these rates will be multiplied by a constant.
+The control arm is assumed to follow an exponential distribution with a median
+of 9 months and the dropout rate is 0.0001 per month regardless of treatment group.
+Finally, the experimental treatment group is piecewise exponential with a
+3-month delayed treatment effect; that is, in the first 3 months HR = 1 and
+the HR is 0.6 thereafter.
+
+```{r}
+alpha <- 0.025
+beta <- 0.1
+ratio <- 1
+
+# Enrollment
+enroll_rate <- define_enroll_rate(
+ duration = c(2, 2, 10),
+ rate = (1:3) / 3
+)
+
+# Failure and dropout
+fail_rate <- define_fail_rate(
+ duration = c(3, Inf),
+ fail_rate = log(2) / 9,
+ hr = c(1, 0.6),
+ dropout_rate = .0001
+)
+# IA and FA analysis time
+analysis_time <- c(20, 36)
+
+# Randomization ratio
+ratio <- 1
+```
+
+We use the null hypothesis information for boundary crossing probability
+calculations under both the null and alternate hypotheses.
+This will also imply the null hypothesis information will be used for the
+information fraction used in spending functions to derive the design.
+
+```{r}
+info_scale <- "h0_info"
+```
+
+# One-sided design {.tabset}
+
+For the design, we have efficacy bounds at both the IA and FA.
+We use the @lan1983discrete spending function with a total alpha of `r alpha`,
+which approximates an O'Brien-Fleming bound.
+
+## Planned design
+
+```{r, echo=TRUE}
+upper <- gs_spending_bound
+upar <- list(sf = gsDesign::sfLDOF, total_spend = alpha, param = NULL)
+
+x <- gs_design_ahr(
+ enroll_rate = enroll_rate,
+ fail_rate = fail_rate,
+ alpha = alpha,
+ beta = beta,
+ info_frac = NULL,
+ info_scale = "h0_info",
+ analysis_time = analysis_time,
+ ratio = ratio,
+ upper = gs_spending_bound,
+ upar = upar,
+ test_upper = TRUE,
+ lower = gs_b,
+ lpar = rep(-Inf, 2),
+ test_lower = FALSE
+) |> to_integer()
+```
+
+The planned design targets:
+
+- Planned events: `r round(x$analysis$event, 0)`
+- Planned information fraction for interim and final analysis: `r round(x$analysis$info_frac, 4)`
+- Planned alpha spending: `r round(gsDesign::sfLDOF(0.025, x$analysis$info_frac)$spend, 4)`
+- Planned efficacy bounds: `r round(x$bound$z[x$bound$bound == "upper"], 4)`
+
+We note that rounding up the final targeted events increases power slightly
+over the targeted 90\%.
+
+```{r}
+x |>
+ summary() |>
+ as_gt() |>
+ gt::tab_header(title = "Original design") |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "lightcyan"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = Z,
+ rows = Bound == "Efficacy"
+ )
+ ) |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "#F9E3D6"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = Z,
+ rows = Bound == "Futility"
+ )
+ )
+```
+
+## Update bounds at time of analysis
+
+We assume 180 and 280 events observed at the IA and FA, respectively.
+We will assume the differences from planned are due to logistical considerations.
+We also assume the protocol specifies that the full $\alpha$ will be spent at
+the final analysis even in a case like this where there is a shortfall of events
+versus the design plan.
+
+```{r, echo=TRUE}
+# Observed vs. planned events
+event_observed <- c(180, 280)
+event_planned <- x$analysis$event
+```
+
+```{r}
+tibble::tibble(
+ Analysis = c("IA", "FA"),
+ `Planned events` = event_planned,
+ `Observed events` = event_observed
+) |>
+ gt::gt() |>
+ gt::tab_header("Planned vs. Observed events")
+```
+
+We will utilize the `gs_power_npe()` function to update efficacy bounds
+based on the observed events.
+The details of its arguments and implementations are explained in the Appendix.
+
+```{r, echo=TRUE}
+# Take spending setup from original design
+upar <- x$upper$upar
+# Now update timing parameter for the interim analysis.
+# Interim spending based on observed events divided by final planned events.
+# The remaining alpha will be allocated to FA.
+upar$timing <- c(event_observed[1] / max(event_planned), 1)
+
+x_updated <- gs_power_npe(
+ # `theta = 0` provides the crossing probability under H0
+ theta = 0,
+ # Observed statistical information under H0
+ info = event_observed * x$input$ratio / (1 + x$input$ratio)^2,
+ info_scale = "h0_info",
+ # Upper bound uses spending function from planned design
+ upper = x$input$upper,
+ upar = x$input$upar,
+ test_upper = x$input$test_upper,
+ # No lower bound, but copy this from input
+ lower = x$input$lower,
+ lpar = x$input$lpar,
+ test_lower = x$input$test_lower,
+ # Binding
+ binding = x$input$binding
+)
+```
+
+The updated efficacy bounds are `r round(x_updated$z[x_updated$bound == "upper"], 3)`.
+
+```{r}
+x_updated |>
+ dplyr::filter(bound == "upper") |>
+ gt::gt() |>
+ gt::tab_header(
+ title = "Updated design",
+ subtitle = paste0("with observed ", paste(event_observed, collapse = ", "), " events")
+ ) |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "lightcyan"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = z,
+ rows = bound == "upper"
+ )
+ ) |>
+ gt::tab_footnote(
+ footnote = "Crossing pbability under H0.",
+ locations = gt::cells_column_labels(columns = probability)
+ )
+```
+
+# Two-sided asymmetric design, beta-spending with non-binding lower bound {.tabset}
+
+In this section, we investigate a 2 sided asymmetric design, with the
+non-binding beta-spending futility bounds. Beta-spending refers to
+error spending for the lower bound crossing probabilities under the
+alternative hypothesis. Non-binding assumes the trial continues if the
+lower bound is crossed for Type I, but not Type II error computation.
+
+## Planned design
+
+In the original designs, we employ the Lan-DeMets spending function used to
+approximate O'Brien-Fleming bounds [@lan1983discrete] for both efficacy and
+futility bounds.
+The total spending for efficacy is `r alpha`, and for futility is `r beta`.
+Besides, we assume the futility test only happens at IA.
+
+```{r, echo=TRUE}
+upper <- gs_spending_bound
+upar <- list(sf = gsDesign::sfLDOF, total_spend = alpha, param = NULL)
+lower <- gs_spending_bound
+lpar <- list(sf = gsDesign::sfLDOF, total_spend = beta, param = NULL)
+
+x <- gs_design_ahr(
+ enroll_rate = enroll_rate,
+ fail_rate = fail_rate,
+ alpha = alpha,
+ beta = beta,
+ info_frac = NULL,
+ info_scale = "h0_info",
+ analysis_time = c(20, 36),
+ ratio = ratio,
+ upper = gs_spending_bound,
+ upar = upar,
+ test_upper = TRUE,
+ lower = lower,
+ lpar = lpar,
+ test_lower = c(TRUE, FALSE),
+ binding = FALSE
+) |> to_integer()
+```
+
+In the planned design, we have
+
+- Planned events: `r round(x$analysis$event, 0)`
+- Planned information fraction (timing): `r round(x$analysis$info_frac, 4)`
+- Planned alpha spending: `r gsDesign::sfLDOF(0.025, x$analysis$info_frac)$spend`
+- Planned efficacy bounds: `r round(x$bound$z[x$bound$bound == "upper"], 4)`
+- Planned futility bounds: `r round(x$bound$z[x$bound$bound == "lower"], 4)`
+
+Since we added futility bounds, the sample size and number of events are
+larger than what we have in the 1-sided example.
+
+```{r}
+x |>
+ summary() |>
+ as_gt() |>
+ gt::tab_header(title = "Original design") |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "lightcyan"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = Z,
+ rows = Bound == "Efficacy"
+ )
+ ) |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "#F9E3D6"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = Z,
+ rows = Bound == "Futility"
+ )
+ )
+```
+
+## Update bounds at time of analysis
+
+In practice, let us assume the observed data is generated by `simtrial::sim_pw_surv()`.
+
+```{r, echo=TRUE}
+set.seed(42)
+
+observed_data <- simtrial::sim_pw_surv(
+ n = x$analysis$n[x$analysis$analysis == 2],
+ stratum = data.frame(stratum = "All", p = 1),
+ block = c(rep("control", 2), rep("experimental", 2)),
+ enroll_rate = x$enroll_rate,
+ fail_rate = (fail_rate |> simtrial::to_sim_pw_surv())$fail_rate,
+ dropout_rate = (fail_rate |> simtrial::to_sim_pw_surv())$dropout_rate
+)
+
+observed_ia_data <- observed_data |> simtrial::cut_data_by_date(analysis_time[1])
+observed_fa_data <- observed_data |> simtrial::cut_data_by_date(analysis_time[2])
+
+# Observed vs. planned events
+event_observed <- c(sum(observed_ia_data$event), sum(observed_fa_data$event))
+event_planned <- x$analysis$event
+```
+
+```{r}
+tibble::tibble(
+ Analysis = c("IA", "FA"),
+ `Planned number of events` = event_planned,
+ `Observed number of events` = event_observed
+) |>
+ gt::gt() |>
+ gt::tab_header("Planned vs. Observed events")
+```
+
+Again, we use `gs_power_npe()` to calculate the updated efficacy and futility bounds.
+The details of its arguments and implementations are explained in the Appendix.
+We initially set up `theta = 0` for crossing probability under the null hypothesis
+and `theta0 = 0` for null hypothesis.
+As for `theta1`, there are 2 options to set it up.
+These 2 options arrive at the same value of `theta1`.
+
+- **Option 1:** `theta1` is the weighted average of the piecewise HR, that is,
+
+$$
+ -\log(\text{AHR}) = -\log(\sum_{i=1}^m w_i \; \text{HR}_m),
+$$
+
+where the weight is decided by the ratio of observed number of events and the
+observed final events.
+In this example, the number of observed events at the first 3 months (HR = 1)
+can be derived as
+
+```{r, echo=TRUE}
+event_first_3_month <- sum(observed_data$fail_time < 3)
+event_first_3_month
+```
+
+and the number of observed events after 3 months (HR = 0.6) are
+
+```{r, echo=TRUE}
+event_after_3_month <- event_observed[2] - event_first_3_month
+event_after_3_month
+```
+
+We can derive `theta1` as
+-[`r event_first_3_month` / `r event_observed[2]` $\times \log(1) +$ `r event_after_3_month` / `r event_observed[2]` $\times \log(0.6)$] = `r round(-(event_first_3_month * log(1) + event_after_3_month * log(0.6)) / event_observed[2], 4)`.
+
+```{r}
+theta1 <- -(event_first_3_month * log(1) + event_after_3_month * log(0.6)) / event_observed[2]
+theta1
+```
+
+- **Option 2:** we use the blinded data to estimate the AHR and `theta1` is the
+ negative logarithm of the estimated AHR.
+
+```{r, echo=TRUE}
+blinded_ahr <- ahr_blinded(
+ surv = survival::Surv(
+ time = observed_fa_data$tte,
+ event = observed_fa_data$event
+ ),
+ intervals = c(3, Inf),
+ hr = c(1, 0.6),
+ ratio = 1
+)
+```
+
+```{r}
+blinded_ahr |>
+ gt::gt() |>
+ gt::tab_header("Blinded estimation of average hazard ratio")
+```
+
+```{r}
+theta1 <- blinded_ahr$theta
+theta1
+```
+
+Both Option 1 and Option 2 yield the same value of `theta1`. Option 1 is applicable
+when the event counts are available, while Option 2 is suitable for scenarios where
+time-to-event data is available, without the corresponding event counts.
+A value of `theta1` is computed with one of the above methods and incorporated below.
+
+```{r, echo=TRUE}
+x_updated <- gs_power_npe(
+ # `theta = 0` provides the crossing probability under H0.
+ # Users have the flexibility to modify the value of `theta`,
+ # if they are interested in the crossing probability under H1 or other scenarios.
+ theta = 0,
+ # -log(AHR) = 0 for H0 is used for determining upper spending
+ theta0 = 0,
+ # -log(AHR) for H1 is used for determining the lower spending and bounds
+ theta1 = theta1,
+ # Observed statistical information under H0 with equal randomization
+ info = event_observed * ratio / (1 + ratio)^2,
+ # Upper bound
+ upper = x$input$upper,
+ upar = list(
+ sf = gsDesign::sfLDOF,
+ total_spend = alpha,
+ param = NULL,
+ # The remaining alpha will be allocated to the FA stage
+ timing = c(event_observed[1] / max(event_planned), 1)
+ ),
+ test_upper = TRUE,
+ # Lower bound
+ lower = x$input$lower,
+ lpar = list(
+ sf = gsDesign::sfLDOF,
+ total_spend = beta,
+ param = NULL,
+ # The remaining alpha will be allocated to the FA stage
+ timing = c(event_observed[1] / max(event_planned), 1)
+ ),
+ test_lower = c(TRUE, FALSE),
+ binding = x$input$binding
+)
+```
+
+```{r}
+x_updated |>
+ dplyr::filter(abs(z) < Inf) |>
+ gt::gt() |>
+ gt::tab_header(title = "Updated design", subtitle = "by event fraction based timing") |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "lightcyan"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = probability,
+ rows = bound == "upper"
+ )
+ ) |>
+ gt::tab_style(
+ style = list(
+ gt::cell_fill(color = "#F9E3D6"),
+ gt::cell_text(weight = "bold")
+ ),
+ locations = gt::cells_body(
+ columns = probability,
+ rows = bound == "lower"
+ )
+ )
+```
+
+# Appendix
+
+## Arguments of `gs_power_npe()`
+
+We provide an introduction to its arguments.
+
+- *3 theta-related arguments*:
+ - `theta`: a vector with the natural parameter for the group sequential design;
+ represents expected ; $-\log(\text{AHR})$ in this case.
+ This is used for boundary crossing probability calculation.
+ For example, if we set `theta = 0`, then the crossing probability is the
+ Type I error.
+ - `theta0`: natural parameter for H0; used for determining the upper
+ (efficacy) spending and bounds. If the default of `NULL` is given,
+ then this is replaced by 0.
+ - `theta1`: natural parameter for H1; used for determining the lower spending
+ and bounds. The default is `NULL`, in which case the value is replaced with
+ the input `theta`.
+
+- *3 statistical information-related arguments*:
+ - `info`: statistical information at all analyses for input `theta`.
+ Default is `1`.
+ - `info0`: statistical information under H0, if different than `info`.
+ It impacts null hypothesis bound calculation. Default is `NULL`.
+ - `info1`: statistical information under hypothesis used for futility bound
+ calculation if different from `info`. It impacts futility hypothesis bound
+ calculation. Default is `NULL`.
+
+- *Efficacy/Futility boundary-related arguments*:
+ - `upper`, `upar` and `test_upper`: efficacy bounds related, same as `gs_design_ahr()`.
+ - `lower`, `lpar` and `test_lower`: futility bounds related, same as `gs_design_ahr()`.
+ - `binding`: `TRUE` or `FALSE` indicating whether it is binding or non-binding designs.
+
+## Explanations of `gs_power_npe()` arguments set up in the one-sided design
+
+We initially set up `theta = 0`,
+which provides the crossing probability under the null hypothesis.
+Users have the flexibility to modify the value of `theta` if they are
+interested in the crossing probability under alternative hypotheses or
+other scenarios. In this implementation, we leave the setup of `theta0`
+with its default imputed value of `0`, indicating the null hypothesis.
+Additionally, as we are solely dealing with efficacy bounds,
+we do not adjust `theta1`, which influences the futility bound.
+
+Moving forward, we proceed to set up the values of the `info` arguments
+for the input `theta = 0`. Since the statistical information is event-based,
+and `theta = 0` (null hypothesis), the observed statistical information
+under null hypothesis is
+
+$$
+ \text{observed number of events} \times \frac{r}{1 + r} \times \frac{1}{1 + r},
+$$
+
+where $r$ is the randomization ratio (experimental : control).
+In this example, the observed statistical information is calculated as
+`info = event_observed / 4`, and thus, we input `r event_observed / 4`
+for the `info` argument. In this case, we retain the default imputed value
+for `info0`, which is set as `info`. The `info` represents the
+statistical information under the null hypothesis (`theta = 0`).
+Furthermore, we leave `info1` as it is, considering our focus on efficacy bounds,
+while noting that `info1` does affect the futility bounds.
+
+Lastly, we establish the parameters for the upper and lower bounds.
+The setup of `upper`, `upar`, `test_upper`, `lower`, `lpar`, and `test_lower`
+closely resembles the original design, with the exception of the `upar` setup.
+Here, we have introduced the `timing` parameter.
+At the IA, the observed number of events is `r event_observed[1]`.
+If we employ the interim analysis alpha spending strategy, the spending timing
+at IA is calculated as `r event_observed[1]` / `r round(event_planned[2], 0)`.
+Here, `r round(event_planned[2], 0)` represents the planned number of events
+at FA. The remaining alpha will be allocated to the FA stage.
+
+## Explanations of `gs_power_npe()` arguments set up in the two-sided asymmetric design, beta-spending with non-binding lower bound
+
+Moving forward, we proceed to set up the values of the `info` arguments for
+the input `theta = 0`. Similar to the examples in the 1-sided design above,
+we set `info` as `r event_observed`/4, where `r event_observed` is the
+observed number of events. Because `info0` is under null hypothesis,
+we leave it and use its default imputed values same as `info`.
+Furthermore, we leave `info1` as its default imputed values same as `info`
+with the local null hypothesis approach. Though people can explicitly write
+the formula of `info1` in terms of the observed events, the difference is
+very minor, and it would be easiest with unblinded data.
+
+Lastly, we establish the parameters for the upper and lower bounds,
+following the similar procedure as we have in the 1-sided design.
+
+# References