---
title: "ANTHEM-HFrEF: a published adaptive survival design"
output:
  rmarkdown::html_vignette:
    self_contained: false
    lib_dir: libs
    css: libs/vignette.css
vignette: >
  %\VignetteIndexEntry{ANTHEM-HFrEF: a published adaptive survival design}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
source("shared-vignette-resources.R")
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4.8
)
set.seed(3425422)
```

```{r setup, message = FALSE}
library(goldilocks)
```

The ANTHEM-HFrEF pivotal study is a published example of Bayesian predictive
sample-size adaptation paired with a conventional frequentist final analysis.
The trial compared vagal nerve stimulation (VNS) plus guideline-directed
medical therapy (GDMT) with GDMT alone in patients with heart failure and
reduced ejection fraction. It was registered as
[NCT03425422](https://clinicaltrials.gov/study/NCT03425422), its design was
published by Konstam et al. (2019), and its results were published by Konstam
et al. (2026).

This vignette maps the morbidity and mortality component of the planned design
to `goldilocks`. It is an explicit approximation, not a reconstruction or
independent validation of the sponsor's analysis. The public protocol, statistical
analysis plan (SAP), and Adaptive Design Report (ADR) disclose substantially
more detail than the journal articles alone, including the predictive model,
priors, simulation profiles, and reference operating characteristics. Even
with those documents, differences between the sponsor's design and the
analyses currently available in `goldilocks` remain important and are
identified below.

## Clinical question and planned analysis

ANTHEM-HFrEF randomized patients 2:1 to VNS plus GDMT or GDMT alone. The
primary efficacy endpoint was time from randomization to cardiovascular death
or first heart-failure hospitalization. The planned maximum sample size was
1,000 patients, and the final primary analysis was a one-sided log-rank test.
Superiority required a one-sided *P* value no larger than 0.019. That nominal
level was calibrated by simulation so that the overall one-sided type I error,
including the adaptive decisions, did not exceed 0.025.

At an interim update, the design calculated two predictive probabilities:

-   $\mathrm{PPS}_n$: probability that the final log-rank test would succeed if
    enrollment stopped at the current sample size and follow-up continued;
-   $\mathrm{PPS}_{\mathrm{max}}$: probability that the final log-rank test
    would succeed if enrollment continued to 1,000 patients.

Futility monitoring began at the first update, nominally at 400 randomized
patients, and stopped the trial if $\mathrm{PPS}_{\mathrm{max}} < 0.01$.
Expected-success monitoring began at 500 patients and stopped enrollment if
$\mathrm{PPS}_n > 0.95$.
Enrollment-based updates then recurred after every additional 100 patients
through 1,000. The reported 400-patient update also required at least 300
patients to have nine months since randomization, and the design included
additional futility updates four and eight months after the last patient was
randomized.

```{r reported-flow, echo = FALSE, message = FALSE, out.width = "100%"}
DiagrammeR::grViz("
digraph anthem_flow {
  graph [rankdir = TB, bgcolor = transparent, ranksep = 0.45, nodesep = 0.30]
  node [shape = box, style = rounded, fontname = Helvetica, fontsize = 13,
        margin = 0.10]
  edge [fontname = Helvetica, fontsize = 11]

  start [label = 'Interim update\nN = 400*, 500, ..., 1000']
  fut [shape = diamond, label = 'PPSmax < 0.01?']
  stopf [label = 'Stop for futility']
  eligible [shape = diamond, label = 'N = 500, ..., 900?']
  succ [shape = diamond, label = 'PPSn > 0.95?']
  stops [label = 'Stop enrollment\nfor expected success']
  more [shape = diamond, label = 'N < 1000?']
  accrue [label = 'Continue enrollment\nto next 100-patient look']
  follow [label = 'Continue follow-up to\ncommon final study visit']
  final [label = 'One-sided log-rank test\nsuccess if P <= 0.019']

  start -> fut
  fut -> stopf [label = 'Yes']
  fut -> eligible [label = 'No']
  eligible -> succ [label = 'Yes']
  eligible -> more [label = 'No']
  succ -> stops [label = 'Yes']
  succ -> more [label = 'No']
  more -> accrue [label = 'Yes']
  accrue -> start
  more -> follow [label = 'No']
  stopf -> follow
  stops -> follow
  follow -> final
}
", width = "100%", height = "760px")
```

The asterisk on 400 denotes the additional nine-month-information condition.
The chart focuses on the morbidity and mortality sample-size rule; the trial
also had safety, symptom, function, and regulatory decision criteria that are
outside this package example.

## Planned design versus operational history

The adaptive design and what actually happened must be kept separate. The
registry reports 533 randomized patients. The final report excluded one
incorrect randomization from the intent-to-treat population, leaving 532
evaluable randomized patients. After the second interim analysis, the data
monitoring committee recommended continuing the trial unchanged, but the
sponsor stopped enrollment and closed the program for reasons outside the
prespecified efficacy and futility rules. The published primary endpoint was
neutral (hazard ratio 0.84, one-sided *P* = 0.115). Nothing in the simulations
below represents or attempts to reproduce that operational decision or the
observed patient data.

## Source and status of every modeled input

The status labels have the following meanings:

-   **reported**: stated numerically in a public primary source;
-   **inferred**: calculated from, or used to encode, reported information;
-   **assumed**: selected for this runnable package example;
-   **unavailable**: needed for exact reproduction but not publicly supplied.

```{r input-audit, echo = FALSE}
input_audit <- data.frame(
  Input = c(
    "`N_total`",
    "`interim_look`",
    "`end_of_study`",
    "`rand_ratio`",
    "`block`",
    "`lambda`, `lambda_time`",
    "`cutpoints`",
    "`generation_cutpoints`",
    "`hazard_control`",
    "`hazard_treatment`",
    "`prop_loss`",
    "`prior_surv`",
    "`alternative`",
    "`h0`",
    "`Fn`",
    "`Sn`",
    "`prob_ha`",
    "`method`",
    "`imputed_final`",
    "`N_impute`",
    "`N_trials`"
  ),
  Value = c(
    "1000",
    "400, 500, 600, 700, 800, 900",
    "16 months (69.33 weeks)",
    "1 control : 2 treatment",
    "3",
    "Six-step ramp to 26 patients/month",
    "6 and 12 months",
    "12 months",
    "0.00828, 0.00240 events/week",
    "0.70 x control hazard",
    "0.10",
    "Gamma shapes 1; rates 1/0.0069, 1/0.0069, 1/0.0035",
    "less",
    "0",
    "0.01 at every modeled look",
    "1.00 at N=400; 0.95 at N=500,...,900",
    "0.981",
    "logrank",
    "FALSE",
    "300 evaluated",
    "20 per evaluated scenario"
  ),
  Status = c(
    "reported", "reported", "reported", "reported", "assumed",
    "inferred", "reported", "reported", "reported", "inferred",
    "reported", "reported", "reported", "reported", "reported",
    "inferred", "inferred", "reported", "inferred", "assumed", "assumed"
  ),
  `Source and mapping` = c(
    "ADR Sections 1.2 and 3",
    "ADR Section 3; maximum N is not an `interim_look` in `goldilocks`",
    "ADR Sections 1.4.1 and 3.3",
    "ADR Section 1.2; named package values identify control and treatment",
    "ADR reports varying blocks of 3, 6, or 9; a fixed block of 3 is the closest available specification",
    "ADR Sections 5.2 and 7 report a six-month ramp to a peak of 26/month",
    "ADR Section 2.1 reports 0-6, 6-12, 12-18, and >18 month intervals; the 16-month package horizon uses the first two cut-points",
    "ADR Table 5 uses 0-12, 12-24, and >24 month generating intervals; only the 12-month cut-point precedes the 16-month horizon",
    "ADR Table 5, using its two generating hazards that apply before the 16-month horizon",
    "ADR Sections 5.1 and 7 report the target hazard-ratio scenario",
    "ADR Sections 5.3 and 7.2; independent exponential dropout CDF of 0.10 at 16 months",
    "ADR Table 1; `goldilocks` applies these independent priors to both arms",
    "ADR Equation 1 defines lower treatment hazard as benefit",
    "ADR Equation 1 uses equality of survival distributions",
    "ADR Section 3.2",
    "ADR Section 3.3; 1.00 disables package success stopping at N=400",
    "1 minus the reported one-sided P-value threshold of 0.019",
    "ADR Section 1.4.1",
    "The reported final analysis uses observed right-censored data",
    "Illustrative setting; ADR Section 2.3.3 specifies at least 10,000 draws for actual interim analyses and 1,000 within design simulations",
    "Illustrative study uses 20 per scenario; ADR Section 5.5 used 1,000 trials per treatment-benefit scenario and 10,000 per null scenario"
  ),
  check.names = FALSE
)

knitr::kable(input_audit, format = "pipe")
```

## Event-time and accrual assumptions

The ADR's control-arm simulation profile with a 35% one-year event probability
used weekly hazards 0.00828 through month 12, 0.00240 from months 12 to 24,
and 0.00012 thereafter. This profile implies a three-year event probability
close to the 43% planning value summarized in the final paper:

```{r event-profile}
weeks_per_month <- 52 / 12

sponsor_control_hazard_week <- c(0.00828, 0.00240, 0.00012)
sponsor_interval_length_week <- rep(52, 3)
control_event_probability_3y <- 1 - exp(-sum(
  sponsor_control_hazard_week * sponsor_interval_length_week
))

data.frame(
  Quantity = c("One-year control event probability", "Three-year control event probability"),
  Value = c(
    1 - exp(-0.00828 * 52),
    control_event_probability_3y
  )
)
```

The data-generating and predictive models use different partitions. We retain
the predictive cut-points at 6 and 12 months and the generating cut-point at 12
months. The reported 18-month predictive cut-point and 24-month generating
cut-point fall beyond this example's 16-month subject-level horizon, so neither
can affect an event time or imputation and both are omitted.

```{r model-parameters}
analysis_cutpoints_week <- c(6, 12) * weeks_per_month
generation_cutpoints_week <- 12 * weeks_per_month
end_of_study_week <- 16 * weeks_per_month

hazard_control_week <- c(0.00828, 0.00240)
hazard_treatment_target_week <- 0.70 * hazard_control_week
hazard_treatment_null_week <- hazard_control_week

prior_surv_approx <- rbind(
  shape = c(1, 1, 1),
  rate = c(1 / 0.0069, 1 / 0.0069, 1 / 0.0035)
)
```

The reported accrual simulation used a Poisson process with a six-month ramp
to a peak of 26 patients per month. `goldilocks` supports piecewise-constant
rather than linear enrollment rates, so the code uses six one-month steps at
the midpoints of the reported linear ramp. The construction preserves the
expected enrollment during the six-month ramp.

```{r accrual-profile}
peak_rate_per_month <- 26
ramp_rate_per_month <- c(
  peak_rate_per_month * seq(1, 11, by = 2) / 12,
  peak_rate_per_month
)
ramp_change_week <- (1:6) * weeks_per_month
ramp_rate_per_week <- ramp_rate_per_month / weeks_per_month

accrual_table <- data.frame(
  `Trial-calendar interval` = c(
    paste0("Month ", 1:6),
    "After month 6"
  ),
  `Approximate patients/month` = ramp_rate_per_month,
  `Patients/week supplied to goldilocks` = ramp_rate_per_week,
  check.names = FALSE
)

knitr::kable(accrual_table, digits = 3)
```

The same approximation can be inspected with the package's enrollment plot.
The expected curve, 20 illustrative Poisson trajectories, the maximum sample
size, and the package's per-participant follow-up setting are shown on the more
interpretable month scale. Interim sample sizes are listed in the input table
above.

```{r accrual-projection, fig.height = 5.2}
plot_enrollment(
  lambda = ramp_rate_per_month,
  lambda_time = 1:6,
  N_total = 1000,
  end_of_study = 16,
  n_sim = 20,
  seed = 3425423,
  time_unit = "months",
  main = "Piecewise-constant accrual approximation"
)
```

## A one-trial `goldilocks` approximation

The ADR generated independent exponential dropout times with 10% cumulative
dropout probability by 16 months. The package now uses the same dropout
distribution: `prop_loss = 0.10` at the 16-month `end_of_study` gives rate
$-\log(0.90)/16$ per month (converted to the weekly unit below). Events before
dropout remain observed, so the actual proportion censored by dropout can be
below 10% and varies across trials. The package's shorter administrative
follow-up for earlier recruits remains a separate approximation.

The expected-success threshold is set to 1 at the 400-patient look. Since the
package stops only when its predictive-probability point estimate is greater
than `Sn`, this disables expected-success stopping at that look while retaining
the futility calculation. `prob_ha = 1 - 0.019 = 0.981` maps the final
one-sided log-rank criterion into the package convention of analyzing `1 - P`.

```{r common-design}
anthem_common <- list(
  cutpoints = analysis_cutpoints_week,
  generation_cutpoints = generation_cutpoints_week,
  N_total = 1000,
  lambda = ramp_rate_per_week,
  lambda_time = ramp_change_week,
  interim_look = seq(400, 900, by = 100),
  end_of_study = end_of_study_week,
  prior_surv = prior_surv_approx,
  block = 3,
  rand_ratio = c(control = 1, treatment = 2),
  prop_loss = 0.10,
  alternative = "less",
  h0 = 0,
  Fn = rep(0.01, 6),
  Sn = c(1, rep(0.95, 5)),
  prob_ha = 0.981,
  N_impute = 300,
  mc_conf_level = 0.95,
  empty_interval = "prior",
  method = "logrank",
  imputed_final = FALSE
)
```

The Bayesian piecewise-exponential posterior supplies predictive event times
at each interim look. Each completed predictive data set is then judged by the
frequentist one-sided log-rank test. Thus, Bayesian prediction determines
whether the current sample size appears adequate or futile, while the final
success criterion remains frequentist.

```{r worked-trial}
set.seed(3425422)

anthem_trial <- do.call(survival_adapt, c(
  anthem_common,
  list(
    hazard_treatment = hazard_treatment_target_week,
    hazard_control = hazard_control_week,
    return_trace = TRUE
  )
))

anthem_trial$summary
```

The trace shows the predictive quantities only at looks reached before a stop.
The 400-patient success threshold of 1 is the package encoding of a
futility-only look.

```{r decision-trace}
trace_display <- anthem_trial$trace[c(
  "planned_N",
  "calendar_time",
  "events_treatment",
  "events_control",
  "ppp_stop_now",
  "success_threshold",
  "ppp_success_at_max",
  "futility_threshold",
  "decision"
)]

knitr::kable(
  trace_display,
  digits = 3,
  col.names = c(
    "N",
    "Time",
    "VNS events",
    "Control events",
    "PPSn",
    "Success cut",
    "PPSmax",
    "Futility cut",
    "Decision"
  )
)
```

```{r decision-trace-plot, fig.height = 8}
plot_trial_trace(anthem_trial)
```

This single simulated path is illustrative. Its selected sample size and final
result are random and are not estimates of power or type I error.

## Small null and alternative simulation

The next two scenarios are intentionally small illustrative simulations: 20
trials under the null hazard ratio of 1 and 20 under the target hazard ratio of
0.70. The summary reports a Monte Carlo standard error and 95% Monte Carlo
interval for every probability, making the numerical imprecision visible.

The evaluated design uses 300 predictive draws per look to keep the vignette
computation manageable. This is not a precision recommendation: it gives the
predictive-probability estimate a resolution of about 0.0033. The sponsor used
1,000 draws per look in its operating-characteristic simulations and at least
10,000 for actual interim analyses. The package reports exact bounds and Monte
Carlo standard errors as diagnostics, but decisions use the point estimate.

```{r small-operating-characteristics}
anthem_alt <- do.call(sim_trials, c(
  anthem_common,
  list(
    hazard_treatment = hazard_treatment_target_week,
    hazard_control = hazard_control_week,
    N_trials = 20,
    ncores = 2,
    seed = 3425430
  )
))

anthem_null <- do.call(sim_trials, c(
  anthem_common,
  list(
    hazard_treatment = hazard_treatment_null_week,
    hazard_control = hazard_control_week,
    N_trials = 20,
    ncores = 2,
    seed = 3425431
  )
))

anthem_oc <- summarise_sims(list(
  "Null: HR = 1.00" = anthem_null,
  "Target: HR = 0.70" = anthem_alt
))

oc_display <- anthem_oc[c(
  "scenario",
  "n_used",
  "power",
  "power_mcse",
  "power_mc_lower",
  "power_mc_upper",
  "stop_success",
  "stop_futility",
  "mean_N",
  "mean_N_mcse"
)]

knitr::kable(
  oc_display,
  digits = 3,
  col.names = c(
    "Scenario", "Trials used", "Power", "Power MCSE",
    "Power lower 95% MC", "Power upper 95% MC", "Expected success stop",
    "Futility stop", "Mean N", "Mean N MCSE"
  )
)
```

For the corresponding sponsor scenario - 35% control event probability at one
year, hazard ratio 0.70, peak accrual 26/month, and 10% dropout - the ADR
reported power 0.836 and mean sample size 833. Under the null with the same
control profile it reported type I error 0.021 and mean sample size 748. Those
results came from the sponsor's modified FACTS analysis, 1,000 alternative trials,
10,000 null trials, and 1,000 predictive iterations per simulated interim.
They are reference targets, not values that a 20-trial vignette simulation can
meaningfully validate.

The journal article summarized the same planning exercise more broadly as
approximately 80% power for hazard ratio 0.70, a three-year control event rate
of about 43%, 26 patients/month, and 10% dropout. The small package results may
differ because of Monte Carlo error and the structural approximations described
next.

## Differences from the sponsor's design

Several distinctions are consequential:

1.  **Predictive model.** The ADR uses common control baseline hazards and one
    shared treatment log hazard ratio with a weakly informative normal prior.
    `goldilocks` estimates independent piecewise hazards for the two arms.
    Arm-specific Gamma priors can represent the reported control-hazard prior,
    but the joint sponsor parameterization and its shared treatment-effect
    prior cannot.
2.  **Look timing.** Package looks occur when an enrollment count is reached.
    It cannot additionally require 300 patients with nine months since
    randomization at the first look, nor schedule futility updates four and
    eight months after accrual ends.
3.  **Follow-up horizon.** `end_of_study` is a per-subject administrative
    horizon. The published trial kept all randomized patients under follow-up
    until the common final visit 16 months after the last randomization, so
    earlier participants could contribute more than 16 months.
4.  **Accrual and randomization.** The stepwise accrual approximation replaces
    a linear six-month ramp. The package also cannot reproduce geographic and
    clinical stratification or randomly varying block sizes 3, 6, and 9.
5.  **Monte Carlo diagnostics.** The public ADR defines each predictive
    probability as the proportion of imputed datasets in which the final test
    succeeds, and compares that point estimate with 0.95 or 0.01. `goldilocks`
    uses the same strict point-estimate comparisons. With 300 imputations, for
    example, expected-success stopping requires at least 286 successes and a
    futility proportion below 0.01 permits zero, one, or two successes. The
    package additionally reports Monte Carlo standard errors and exact
    one-sided bounds in the decision trace, but these are diagnostic only.

These differences concern the statistical and operational scope of the two
designs. They are why the vignette compares broad behavior and operating
characteristics without claiming exact calibration.

## References

Broglio KR, Connor JT, Berry SM. Not too big, not too small: a Goldilocks
approach to sample size selection. *Journal of Biopharmaceutical Statistics*.
2014;24(3):685-705. doi:10.1080/10543406.2014.888569.

ClinicalTrials.gov. Autonomic Regulation Therapy to Enhance Myocardial Function
and Reduce Progression of Heart Failure With Reduced Ejection Fraction.
[NCT03425422](https://clinicaltrials.gov/study/NCT03425422).

Konstam MA, Udelson JE, Butler J, et al. Impact of autonomic regulation therapy
in patients with heart failure: ANTHEM-HFrEF pivotal study design.
*Circulation: Heart Failure*. 2019;12:e005879.
[doi:10.1161/CIRCHEARTFAILURE.119.005879](https://doi.org/10.1161/CIRCHEARTFAILURE.119.005879).

Konstam MA, Udelson JE, Mann DL, et al. Vagal nerve stimulation in patients
with heart failure and reduced ejection fraction: the ANTHEM-HFrEF trial.
*Journal of the American College of Cardiology*. 2026;87(25).
[doi:10.1016/j.jacc.2026.03.040](https://doi.org/10.1016/j.jacc.2026.03.040).

LivaNova USA. *ANTHEM-HFrEF Clinical Investigation Plan*, version 9.2, 1
November 2021. [Public JACC supplement](https://ars.els-cdn.com/content/image/1-s2.0-S0735109726058365-mmc2.pdf).

LivaNova USA. *ANTHEM-HFrEF Statistical Analysis Plan*, version 2.0, 8 February
2022. [Public JACC supplement](https://ars.els-cdn.com/content/image/1-s2.0-S0735109726058365-mmc3.pdf).

Berry Consultants. *ANTHEM-HFrEF Pivotal Trial Adaptive Design Report*,
version 3.2, 13 October 2021. [Public JACC supplement](https://ars.els-cdn.com/content/image/1-s2.0-S0735109726058365-mmc4.pdf).

LivaNova USA. *Statistical Analysis Plan Amendment - ANTHEM-HFrEF Pivotal
Study*, version 1.0, 16 May 2023. [Public JACC supplement](https://ars.els-cdn.com/content/image/1-s2.0-S0735109726058365-mmc5.pdf).
