# Longitudinal Data MCP-Mod

In this vignette we illustrate how to use the `DoseFinding` package with longitudinal, continuous observations by fitting a mixed model for repeated measures (MMRM) and applying the generalized MCP-Mod methodology to the resulting estimates. We also show how to apply futility analyses during the trial, following Bornkamp et al. (2024).

    library(DoseFinding)

## Data Simulation Code

In order to illustrate the methodology, we are going to consider data as simulated in Section 4.1 of Bornkamp et al. (2024). We include the corresponding functions here for completeness. This can be helpful for users who want to simulate their own data during trial design.

### Calculate Mean Response from Emax Model

The `calcMean` function calculates the mean response at each dose and each time point under the Emax model with \(\text{E}_0 = 0\) and \(\text{ED}_{50} = 1\) with the \(\text{E}_{max}\) parameter gradually increasing so that a pre-specified maximum effect is achieved at the last visit. The function takes three arguments: `dose` is a vector of doses, `times` is a vector of time points, and `maxEff` is the maximum effect at the final analysis (i.e., for the highest dose at the last visit). It returns a matrix of mean responses, where the rows correspond to doses and the columns correspond to time points.

    calcMean <- function(dose, times, maxEff) {
      emaxLastVisit <- Mods(emax = 1, doses = dose, maxEff = maxEff)$emax["eMax"]
      maxTime <- max(times)
      outer(dose, times, function(doses, times) {
        Emax <- emaxLastVisit * (1 - exp(-0.5 * times)) / (1 - exp(-0.5 * maxTime))
        Emax * doses / (doses + 1)
      })
    }

    # Let's try it out:
    calcMean(
      dose = c(0, 0.5, 1, 2, 4),
      times = 0:10,
      maxEff = 0.1
    )

         [,1]       [,2]       [,3]       [,4]       [,5]       [,6]       [,7]
    [1,]    0 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000
    [2,]    0 0.01650577 0.02651703 0.03258916 0.03627210 0.03850591 0.03986079
    [3,]    0 0.02475866 0.03977554 0.04888374 0.05440814 0.05775886 0.05979118
    [4,]    0 0.03301154 0.05303405 0.06517832 0.07254419 0.07701182 0.07972157
    [5,]    0 0.03961385 0.06364086 0.07821399 0.08705303 0.09241418 0.09566588
               [,8]       [,9]      [,10]      [,11]
    [1,] 0.00000000 0.00000000 0.00000000 0.00000000
    [2,] 0.04068256 0.04118099 0.04148330 0.04166667
    [3,] 0.06102384 0.06177149 0.06222496 0.06250000
    [4,] 0.08136512 0.08236198 0.08296661 0.08333333
    [5,] 0.09763814 0.09883438 0.09955993 0.10000000

### Generate Normal Mean Vector and Covariance Matrix

The `parGen` function returns the mean and (compound-symmetry) covariance matrix for the continuous outcomes, given the same input as for `calcMean` above, plus the baseline mean `bslMean`, and the standard deviation `errSD` and constant correlation `rho` of the residuals:

    parGen <- function(times, doses, maxEff, bslMean = 0, errSD = 0.55, rho = 0.9) {
      times <- sort(times)
      ndim <- length(times)

      ## matrix of outcome means for each dose and visit
      MeanMat <- calcMean(doses, times, maxEff) + bslMean
      rownames(MeanMat) <- paste0("Dose", doses)
      colnames(MeanMat) <- paste0("Week", times)

      ## cov mat. for err
      sdV <- if (length(errSD) == 1) {
        rep(errSD, ndim)
      } else if (length(errSD) == ndim) {
        errSD
      } else {
        stop("Length of err sd should either be 1 (homogeneous) or equal to the number of visits in times")
      }

      ## CS covariance structure
      R <- diag(1, ndim)
      R[lower.tri(R)] <- R[upper.tri(R)] <- rho
      Sigm <- diag(sdV) %*% R %*% diag(sdV)

      list(visit = times, Sigm = Sigm, MeanMat = MeanMat, bslMean = bslMean)
    }

    # Let's try it out:
    pars <- parGen(
      times = 0:10,
      doses = c(0, 0.5, 1, 2, 4),
      maxEff = 0.1,
      bslMean = 1.5
    )
    pars

    $visit
     [1]  0  1  2  3  4  5  6  7  8  9 10

    $Sigm
             [,1]    [,2]    [,3]    [,4]    [,5]    [,6]    [,7]    [,8]    [,9]
     [1,] 0.30250 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225
     [2,] 0.27225 0.30250 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225
     [3,] 0.27225 0.27225 0.30250 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225
     [4,] 0.27225 0.27225 0.27225 0.30250 0.27225 0.27225 0.27225 0.27225 0.27225
     [5,] 0.27225 0.27225 0.27225 0.27225 0.30250 0.27225 0.27225 0.27225 0.27225
     [6,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.30250 0.27225 0.27225 0.27225
     [7,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.30250 0.27225 0.27225
     [8,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.30250 0.27225
     [9,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.30250
    [10,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225
    [11,] 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225 0.27225
            [,10]   [,11]
     [1,] 0.27225 0.27225
     [2,] 0.27225 0.27225
     [3,] 0.27225 0.27225
     [4,] 0.27225 0.27225
     [5,] 0.27225 0.27225
     [6,] 0.27225 0.27225
     [7,] 0.27225 0.27225
     [8,] 0.27225 0.27225
     [9,] 0.27225 0.27225
    [10,] 0.30250 0.27225
    [11,] 0.27225 0.30250

    $MeanMat
            Week0    Week1    Week2    Week3    Week4    Week5    Week6    Week7
    Dose0     1.5 1.500000 1.500000 1.500000 1.500000 1.500000 1.500000 1.500000
    Dose0.5   1.5 1.516506 1.526517 1.532589 1.536272 1.538506 1.539861 1.540683
    Dose1     1.5 1.524759 1.539776 1.548884 1.554408 1.557759 1.559791 1.561024
    Dose2     1.5 1.533012 1.553034 1.565178 1.572544 1.577012 1.579722 1.581365
    Dose4     1.5 1.539614 1.563641 1.578214 1.587053 1.592414 1.595666 1.597638
               Week8    Week9   Week10
    Dose0   1.500000 1.500000 1.500000
    Dose0.5 1.541181 1.541483 1.541667
    Dose1   1.561771 1.562225 1.562500
    Dose2   1.582362 1.582967 1.583333
    Dose4   1.598834 1.599560 1.600000

    $bslMean
    [1] 1.5

Note that we assume here that the time is in weeks, but that could of course easily be adapted as needed for your project.

### Simulate Longitudinal Outcomes

The `datGen` function simulates the longitudinal outcome data, given a sample size of patients `N`, a list of parameters `pars` (as generated by `parGen`), a vector of `doses` and corresponding allocation ratios `alRatio`, the enrollment time of the last patient `LPFV.t`. The distribution of the recruitment time can be specified by `RecrDist` and `RandRecr`, with the following options:

- `RecrDist = "Quad"`: quadratic, \(a t^2\), where \(a = N / LPFV.t^2\).
  - `RandRecr = TRUE`: \(t \sim \sqrt{N \cdot \text{Unif}(0,1) / a}\).
  - `RandRecr = FALSE`: \(t = \sqrt{i / a}\), where \(i = 1, \dotsc, N\).
- `Exp`: exponential, with rate \(\lambda = -\log(0.9 / N) / LPFV.t\).
  - `RandRecr = TRUE`: \(t \sim \text{Exp}(\lambda)\).
  - `RandRecr = FALSE`: \(t = F_{\text{Exp}(\lambda)}^{-1}(i/N)\) for \(i = 1, \dotsc, N-1\); \(t = LPFV.t\) for \(i = N\).
- `Unif`: uniform, \(t \sim \text{Unif}(0, LPFV.t)\).
  - `RandRecr = TRUE`: \(t \sim \text{Unif}(0, LPFV.t)\).
  - `RandRecr = FALSE`: \(t = i \cdot LPFV.t / N\) for \(i = 1, \dotsc, N\).

    datGen <- function(N = 300, pars, doses, alRatio, LPFV.t, RecrDist = "Quad", RandRecr = TRUE) {
      K <- length(doses)
      nt <- length(pars$visit)

      ## generate list of dose arms
      nblk <- ceiling(N / sum(alRatio)) # number of blocks
      d <- unlist(lapply(1:nblk, function(i) sample(rep(1:K, alRatio), sum(alRatio))))
      d <- d[1:N]

      err <- mvtnorm::rmvnorm(N, mean = rep(0, nt), sigma = pars$Sigm) # error term

      Y <- pars$MeanMat[d, ] + err # complete outcomes

      ## enrollment time (week)
      randT <- if (RecrDist == "Quad") {
        a <- N / LPFV.t^2
        if (RandRecr) {
          sqrt(N * runif(N) / a)
        } else {
          sqrt(c(1:N) / a)
        }
      } else if (RecrDist == "Exp") {
        lamb <- -log(0.9 / N) / LPFV.t
        if (RandRecr) {
          rexp(N, rate = lamb)
        } else {
          c(qexp((1:(N - 1)) / N, lamb), LPFV.t)
        }
      } else if (RecrDist == "Unif") {
        if (RandRecr) {
          runif(N, 0, LPFV.t)
        } else {
          (1:N) * LPFV.t / N
        }
      }

      trdat <- cbind(1:N, doses[d], randT, Y)
      colnames(trdat)[1:3] <- c("SUBJID", "Dose", "enroll.time")
      out <- tibble::as_tibble(trdat) |>
        tidyr::gather("Visit", "response", -c("SUBJID", "Dose", "enroll.time")) |>
        dplyr::arrange(SUBJID)
      out$week <- rep(pars$visit, N)
      out$cal.time <- out$enroll.time + out$week

      bsl <- out |>
        dplyr::filter(week <= 0) |>
        dplyr::select("SUBJID", "response") |>
        dplyr::rename(base = "response")
      out <- bsl |>
        dplyr::left_join(out, by = "SUBJID", multiple = "all") |>
        dplyr::mutate(chg = response - base) |>
        dplyr::select(- dplyr::all_of("response")) |>
        dplyr::rename(response = "chg")

      out |>
        dplyr::mutate(USUBJID = paste0("PAT", SUBJID)) |>
        dplyr::mutate_at("Visit", function(x) factor(x, levels = paste0("Week", pars$visit))) |>
        dplyr::mutate_at("Dose", function(x) factor(x, levels = doses)) |>
        dplyr::arrange(SUBJID, week)
    }

    # Let's try it out:
    dat <- datGen(
      N = 300,
      pars = pars,
      doses = c(0, 0.5, 1, 2, 4),
      alRatio = c(1, 1, 1, 1, 1),
      LPFV.t = 10,
      RecrDist = "Quad",
      RandRecr = TRUE
    )
    head(dat, 15)

    # A tibble: 15 × 9
       SUBJID  base Dose  enroll.time Visit   week cal.time response USUBJID
        <dbl> <dbl> <fct>       <dbl> <fct>  <int>    <dbl>    <dbl> <chr>
     1      1  1.78 0.5          8.69 Week0      0     8.69    0     PAT1
     2      1  1.78 0.5          8.69 Week1      1     9.69   -0.653 PAT1
     3      1  1.78 0.5          8.69 Week2      2    10.7    -0.221 PAT1
     4      1  1.78 0.5          8.69 Week3      3    11.7     0.136 PAT1
     5      1  1.78 0.5          8.69 Week4      4    12.7     0.224 PAT1
     6      1  1.78 0.5          8.69 Week5      5    13.7    -0.420 PAT1
     7      1  1.78 0.5          8.69 Week6      6    14.7    -0.125 PAT1
     8      1  1.78 0.5          8.69 Week7      7    15.7    -0.442 PAT1
     9      1  1.78 0.5          8.69 Week8      8    16.7     0.305 PAT1
    10      1  1.78 0.5          8.69 Week9      9    17.7    -0.313 PAT1
    11      1  1.78 0.5          8.69 Week10    10    18.7     0.142 PAT1
    12      2  1.92 0            7.68 Week0      0     7.68    0     PAT2
    13      2  1.92 0            7.68 Week1      1     8.68   -0.242 PAT2
    14      2  1.92 0            7.68 Week2      2     9.68   -0.307 PAT2
    15      2  1.92 0            7.68 Week3      3    10.7    -0.306 PAT2

So we see that the function generates a `data.frame` with the subject ID, baseline value, dose, enrollment time, visit and week plus calendar time for each longitudinal observation.

### Cut Interim Data

The `intDat` function cuts the interim data when a certain proportion `pct` of patients have their final outcome. It returns a list with the interim analysis time `int.time`, the cut data `dat` and the distribution of the last visit `dist` (i.e., the number of patients with their last visit at each time point).

    intDat <- function(data, pct = 0.5) {
      N <- max(data$SUBJID)
      tmax <- max(data$week)
      sorted_final_cal_time <- data |>
        dplyr::filter(week == tmax) |>
        dplyr::pull(cal.time) |>
        sort()
      N_required <- ceiling(pct * N)
      t.cut <- sorted_final_cal_time[N_required]
      out <- list(int.time = t.cut)
      out$dat <- subset(data, cal.time <= t.cut)
      out$dist <- out$dat |>
        dplyr::group_by(SUBJID) |>
        dplyr::summarize(lastvis = max(week)) |>
        dplyr::group_by(lastvis) |>
        dplyr::summarize(
            n = dplyr::n(),
            pct = (100 * dplyr::n() / N)
        )
      out
    }

    # Let's try it out:
    int_dat <- intDat(dat, pct = 0.5)
    int_dat

    $int.time
    [1] 17.08316

    $dat
    # A tibble: 3,003 × 9
       SUBJID  base Dose  enroll.time Visit  week cal.time response USUBJID
        <dbl> <dbl> <fct>       <dbl> <fct> <int>    <dbl>    <dbl> <chr>
     1      1  1.78 0.5          8.69 Week0     0     8.69    0     PAT1
     2      1  1.78 0.5          8.69 Week1     1     9.69   -0.653 PAT1
     3      1  1.78 0.5          8.69 Week2     2    10.7    -0.221 PAT1
     4      1  1.78 0.5          8.69 Week3     3    11.7     0.136 PAT1
     5      1  1.78 0.5          8.69 Week4     4    12.7     0.224 PAT1
     6      1  1.78 0.5          8.69 Week5     5    13.7    -0.420 PAT1
     7      1  1.78 0.5          8.69 Week6     6    14.7    -0.125 PAT1
     8      1  1.78 0.5          8.69 Week7     7    15.7    -0.442 PAT1
     9      1  1.78 0.5          8.69 Week8     8    16.7     0.305 PAT1
    10      2  1.92 0            7.68 Week0     0     7.68    0     PAT2
    # ℹ 2,993 more rows

    $dist
    # A tibble: 4 × 3
      lastvis     n   pct
        <int> <int> <dbl>
    1       7    49  16.3
    2       8    49  16.3
    3       9    52  17.3
    4      10   150  50

## Longitudinal Data Analysis

Given a longitudinal dataset (at an interim analysis), we can perform two analyses:

1. Completers analysis: Subset the data to patients who have reached the last visit, and then apply a simple linear model.
2. Repeated measures analysis: Fit a mixed model for repeated measures (MMRM) to the data.

In both cases, we then calculate the least square means \(\widehat{\mu}_{0t}\) and the covariance matrix \(S_{0t}\) of the least square means, as well as the residual standard deviation \(\sigma\) of the model.

Let’s code these in two corresponding functions:

### Completers Analysis

    analyzeCompleters <- function(dat, eval.time = "Week12") {
      complDat <- dat |>
        dplyr::filter(Visit == eval.time)
      fit <- lm(response ~ Dose + base, data = complDat)
      lsm <- emmeans::emmeans(fit, ~ Dose)
      summ <- subset(summary(lsm))
      rowIDs <- as.numeric(rownames(summ))
      list(
        mu0t = summ$emmean,
        sigma = sigma(fit),
        S0t = vcov(lsm)
      )
    }

    # Let's try it out:
    resultCompleters <- analyzeCompleters(
      dat = int_dat$dat,
      eval.time = "Week10"
    )
    resultCompleters

    $mu0t
    [1] 0.027117535 0.009118393 0.105201847 0.052822933 0.138610940

    $sigma
    [1] 0.2481969

    $S0t
                    0           0.5             1             2             4
    0    1.991855e-03  3.221810e-06 -2.208779e-06  8.119581e-06 -1.270757e-05
    0.5  3.221810e-06  1.927259e-03 -1.512849e-06  5.561308e-06 -8.703735e-06
    1   -2.208779e-06 -1.512849e-06  1.926090e-03 -3.812669e-06  5.967025e-06
    2    8.119581e-06  5.561308e-06 -3.812669e-06  2.478083e-03 -2.193508e-05
    4   -1.270757e-05 -8.703735e-06  5.967025e-06 -2.193508e-05  2.087719e-03

### Repeated Measures Analysis

    analyzeRepeated <- function(dat, eval.time = "Week12") {
      form <- "response ~  Visit + Dose + base + Dose*Visit + base*Visit"
      postBaselineDat <- dat |>
        dplyr::filter(week > 0) |>
        droplevels()
      fit <- mmrm::mmrm(as.formula(paste0(form, " + us(Visit | USUBJID)")), data = postBaselineDat)
      lsm <- emmeans::emmeans(fit, ~ Dose + Visit)
      summ <- subset(summary(lsm), Visit == eval.time)
      rowIDs <- as.numeric(rownames(summ))
      list(
        mu0t = summ$emmean,
        sigma = sqrt(diag(mmrm::VarCorr(fit)))[eval.time],
        S0t = vcov(lsm)[rowIDs, rowIDs]
      )
    }

    # Let's try it out:
    resultRepeated <- analyzeRepeated(
      dat = int_dat$dat,
      eval.time = "Week10"
    )
    resultRepeated

    $mu0t
    [1] 0.008607189 0.017659190 0.102106486 0.081538734 0.162164897

    $sigma
       Week10
    0.2437212

    $S0t
                    0 Week10    0.5 Week10      1 Week10      2 Week10
    0 Week10    1.461548e-03  1.209716e-06 -1.381468e-06  3.593112e-06
    0.5 Week10  1.209716e-06  1.436477e-03 -6.873193e-07  2.096943e-06
    1 Week10   -1.381468e-06 -6.873193e-07  1.426125e-03 -2.107403e-06
    2 Week10    3.593112e-06  2.096943e-06 -2.107403e-06  1.692186e-03
    4 Week10   -6.027456e-06 -3.486542e-06  3.554832e-06 -1.056883e-05
                    4 Week10
    0 Week10   -6.027456e-06
    0.5 Week10 -3.486542e-06
    1 Week10    3.554832e-06
    2 Week10   -1.056883e-05
    4 Week10    1.516377e-03

## Generalized MCP-Mod

### Final Analysis

At the final analysis, we would like to use the generalized MCP-Mod methodology to test the hypothesis of a dose-response relationship. This is easily done by providing the least square means via `resp` and their covariance matrix via `S` to the `MCTtest` function, which will then perform the MCP-Mod analysis. We just need to also specify the dose levels and the models we want to test: Here we just use three models, it could be more in practice.

    doses <- c(0, 0.5, 1, 2, 4)
    models <- Mods(
      emax = 2,
      sigEmax = c(0.5, 3),
      quadratic = -0.2,
      doses = doses
    )

    resultFinal <- analyzeRepeated(
      dat = dat,
      eval.time = "Week10"
    )

    testFinal <- MCTtest(
      dose = doses,
      resp = resultFinal$mu0t,
      S = resultFinal$S0t,
      models = models,
      alternative = "one.sided",
      type = "general",
      critV = TRUE,
      pVal = TRUE,
      alpha = 0.025
    )

    testFinal

    Multiple Contrast Test

    Contrasts:
          emax sigEmax quadratic
    0   -0.657  -0.788    -0.722
    0.5 -0.271  -0.204    -0.222
    1   -0.013   0.250     0.166
    2    0.310   0.363     0.612
    4    0.632   0.378     0.166

    Contrast Correlation:
               emax sigEmax quadratic
    emax      1.000   0.921     0.827
    sigEmax   0.921   1.000     0.941
    quadratic 0.827   0.941     1.000

    Multiple Contrast Test:
              t-Stat   adj-p
    emax       3.461 < 0.001
    sigEmax    3.286 < 0.001
    quadratic  2.778 0.00498

    Critical value: 2.179 (alpha = 0.025, one-sided)

    testFinal$critV

    [1] 2.178844
    attr(,"Calc")
    [1] TRUE

    testFinal$tStat

         emax   sigEmax quadratic
     3.460999  3.285689  2.778312
    attr(,"pVal")
    [1] 0.0004862437 0.0008942596 0.0049845942

    attr(testFinal$tStat, "pVal")

    [1] 0.0004862437 0.0008942596 0.0049845942

### Futility Interim Analysis

Here we show how to apply the futility analysis during the trial, as described in Bornkamp et al. (2024). The idea is that based on the available interim data, we calculate the predictive or conditional power to detect a significant dose-response relationship at the final analysis. If this value is below a certain threshold, we may want to stop the trial for futility. We can use the `powMCTInterim` function to calculate these values. To do so, we need two additional ingredients:

1. The contrast matrix for the models we want to test, which is provided via the argument `contMat`.
2. The covariance matrix at the final analysis, denoted by \(S_{[0,1]}\) in Bornkamp et al. (2024) and provided via the argument `S_01` here. Here we assume that this matrix is diagonal, with constant entries \(\sigma^2 / n\), where \(\sigma\) is the residual standard deviation of the model and \(n\) is the number of patients in the final analysis.
3. For the conditional power, we also need to specify the assumed dose group means at the primary analysis time-point. This is provided via the argument `mu_assumed`. Typically, this is based on adding the observed placebo response rate to the originally planned treatment effect (before study start).

Let’s calculate these now for the example interim data from above:

    w <- rep(1, length(doses))
    contMat <- optContr(models = models, w = w)$contMat

    n_final <- rep(60, length(doses))
    S01 <- diag(resultRepeated$sigma^2 / n_final)

    # Predictive power:
    predPower <- powMCTInterim(
      contMat = contMat,,
      mu_0t = resultRepeated$mu0t,
      S_0t = resultRepeated$S0t,
      S_01 = S01,
      alpha = 0.025,
      type = "predictive",
      alternative = "one.sided"
    )
    predPower

    [1] 0.9829986
    attr(,"error")
    [1] 4.060934e-06
    attr(,"msg")
    [1] "Normal Completion"

    # Conditional power:
    deltaAssumed <- pars$MeanMat[, "Week10"] - pars$bslMean
    deltaAssumed <- deltaAssumed - deltaAssumed[1]  # assumed treatment difference vs placebo
    mu_assumed <- resultRepeated$mu0t[1] + deltaAssumed # to obtain group means add observed placebo response

    condPower <- powMCTInterim(
      contMat = contMat,
      mu_0t = resultRepeated$mu0t,
      S_0t = resultRepeated$S0t,
      S_01 = S01,
      mu_assumed = mu_assumed,
      alpha = 0.025,
      type = "conditional",
      alternative = "one.sided"
    )
    condPower

    [1] 0.9720396
    attr(,"error")
    [1] 8.29779e-06
    attr(,"msg")
    [1] "Normal Completion"

## Simulation study

In practice, it will in most cases be important to run a simulation study to evaluate the performance of the proposed combination of generalized MCP-Mod with MMRM. In particular, in order to calibrate the futility threshold for the interim analysis, it is important to understand the power loss at the final analysis in relation to the futility threshold. The following code may serve as the starting point for such a simulation study, and can be adapted as needed. We include here the argument documentation in `roxygen2` format for simplicity. The code can also be used to reproduce the simulation study in Section 4.1 of Bornkamp et al. (2024).

    ##' @param times Vector assessment times
    ##' @param bslMean Mean at baseline
    ##' @param errSD Residual standard deviation (on absolute scale)
    ##' @param rho Correlation of measurements over time (within patient)
    ##' @param maxEff Maximum effect achieved at last time-point for the highest dose
    ##' @param RecrDist Recruitment distribution (see function datGen)
    ##' @param LPFV.t Time for the last patient first visit
    ##' @param models DoseFinding::Mods object
    ##' @param ia_timing Information time when IAs are performed (% of patients having last visit)
    ##' @param N Overall sample size
    ##' @param doses doses to use
    ##' @param alRatio allocation ratios to the different doses
    ##' @param eval.time Name of final
    ##' @param alpha Type 1 error for test
    ##' @param nSim Number of simulations
    ##' @param delta_assumed Vector of treatment effects assumed at the different doses
    ##' @return Data frame with all results
    sim_one_trial <- function(times, bslMean, errSD, rho, maxEff,
                              RecrDist, LPFV.t, models, ia_timing = seq(0.3, 0.8, 0.05), N = 531,
                              doses = c(0, 0.5, 1, 2, 4, 8), alRatio = c(2, 1, 1, 1, 2, 2),
                              eval.time = "Week12", alpha = 0.025, nSim = 1000, delta_assumed) {
      contMat <- optContr(models = models, w = alRatio / sum(alRatio))$contMat
      pars <- parGen(times, doses,
        maxEff = maxEff, bslMean = bslMean, errSD = errSD,
        rho = rho
      )
      mydat <- datGen(N, pars, doses, alRatio, LPFV.t = LPFV.t, RecrDist = RecrDist, RandRecr = TRUE)

      ## fit final data
      fit.final <- analyzeCompleters(mydat)
      test <- MCTtest(
        dose = doses, resp = fit.final$mu0t, S = fit.final$S0t, models = models,
        alternative = "one.sided", type = "general", critV = TRUE, pVal = FALSE,
        alpha = alpha
      )
      maxT <- max(test$tStat)
      cVal <- test$critVal

      n_final <- mydat |>
        dplyr::filter(Visit == eval.time) |>
        dplyr::group_by(Dose) |>
        dplyr::count()

      IAtm <- pp_long <- cp_long <- cp_long2 <- pp_compl <- cp_compl <- cp_compl2 <- inf_long <- inf_compl <- numeric()
      IAdist <- matrix(nrow = length(times), ncol = length(ia_timing))

      ## fit interim data
      for (ia in seq_along(ia_timing)) {
        tp <- ia_timing[ia] # prop pts completes visit of eval.time
        cutDat <- intDat(mydat, tp)
        middat <- cutDat$dat
        IAtm[ia] <- cutDat$int.time
        IAdist[times %in% cutDat$dist$lastvis, ia] <- cutDat$dist$pct

        ## using all data from every patient (longitudinal MMRM)
        fit_int_long <- analyzeRepeated(middat, eval.time = eval.time)
        ## only using the completers by visit eval time (cross-sectional linear model)
        fit_int_compl <- analyzeCompleters(middat, eval.time = eval.time)
        ## The covariance matrix anticipated for the estimates at EoS
        S_end <- diag(fit_int_long$sigma^2 / n_final$n)

        # information fraction
        inf_long[ia] <- det(diag(fit_int_long$sigma^2 / n_final$n))^(1 / length(doses)) /
          det(fit_int_long$S0t)^(1 / length(doses))
        inf_compl[ia] <- det(diag(fit_int_compl$sigma^2 / n_final$n))^(1 / length(doses)) /
          det(fit_int_compl$S0t)^(1 / length(doses))

        mu_assumed <- fit_int_long$mu0t[1] + delta_assumed # for conditional power use planned treatment effect
        pp_long[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_long$S0t,
          S_01 = S_end, mu_0t = fit_int_long$mu0t, type = "predictive", alpha = alpha
        )
        cp_long[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_long$S0t,
          S_01 = S_end, mu_0t = fit_int_long$mu0t, type = "conditional", alpha = alpha,
          mu_assumed = mu_assumed
        )
        cp_long2[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_long$S0t,
          S_01 = S_end, mu_0t = fit_int_long$mu0t, type = "conditional", alpha = alpha,
          mu_assumed = fit_int_long$mu0t
        )
        pp_compl[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_compl$S0t,
          S_01 = S_end, mu_0t = fit_int_compl$mu0t, type = "predictive", alpha = alpha
        )
        cp_compl[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_compl$S0t,
          S_01 = S_end, mu_0t = fit_int_compl$mu0t, type = "conditional", alpha = alpha,
          mu_assumed = mu_assumed
        )
        cp_compl2[ia] <- powMCTInterim(
          contMat = contMat, S_0t = fit_int_compl$S0t,
          S_01 = S_end, mu_0t = fit_int_compl$mu0t, type = "conditional", alpha = alpha,
          mu_assumed = fit_int_compl$mu0t
        )
      }
      data.frame(
        final_maxT = maxT, final_cVal = as.numeric(cVal),
        ia_timing, pp_long, cp_long, cp_long2, pp_compl, cp_compl, cp_compl2, inf_long, inf_compl
      )
    }

    ## Function to run one scenario (arguments described in previous function)
    run_scen <- function(n_sim, rho, maxEff,
                         LPFV.t, ia_timing, delta_assumed) {
      ## fixed parameters
      doses <- c(0, 0.5, 1, 2, 4, 8) # doses
      alRatio <- c(2, 1, 1, 1, 2, 2) # allocation ratio for doses
      alpha <- 0.025
      times <- c(0, 2, 4, 8, 12)
      bslMean <- 0
      errSD <- 0.56
      RecrDist <- "Quad"

      if (rho == 0.6 & LPFV.t == 50) N <- 820
      if (rho == 0.6 & LPFV.t == 100) N <- 825
      if (rho == 0.9 & LPFV.t == 50) N <- 248
      if (rho == 0.9 & LPFV.t == 100) N <- 236

      models <- Mods(
        emax = c(0.5, 1, 2, 4), sigEmax = rbind(c(0.5, 3), c(1, 3), c(2, 3), c(4, 3)),
        quadratic = -0.1, doses = doses
      )

      lst <- vector("list", n_sim)
      for (i in 1:n_sim) {
        res <- try(sim_one_trial(
          times = times, bslMean = bslMean,
          errSD = errSD, rho = rho, maxEff = maxEff, RecrDist = RecrDist,
          LPFV.t = LPFV.t, models = models, ia_timing = ia_timing, N = N,
          doses = doses, alRatio = alRatio, delta_assumed = delta_assumed,
          alpha = alpha
        ), silent = FALSE)
        if (!is.character(res)) {
          lst[[i]] <- data.frame(sim = i, res)
        }
      }

      out <- do.call("rbind", lst)
      out$rho <- rho
      out$maxEff <- maxEff
      out$RecrDist <- RecrDist
      out$N <- N
      out$LPFV.t <- LPFV.t
      out
    }

These functions can be used as follows:

    doses <- c(0, 0.5, 1, 2, 4, 8) # doses
    alRatio <- c(2, 1, 1, 1, 2, 2) # allocation ratio for doses
    alpha <- 0.025 # for MCTtest
    times <- c(0, 2, 4, 8, 12) # observation times (weeks)
    bslMean <- 0 # mean at baseline
    errSD <- 0.56 # SD for outcome on absolute scale
    RecrDist <- "Quad" # recruitment distribution

    rho <- 0.9 # correlation across time for outcome on absolute scale
    LPFV.t <- 100 # time of last patient first visit
    maxEff <- 0.12 # effect assumed for the highest dose at the last time point
    delta_assumed <- calcMean(doses, times, maxEff)[, length(times)]
    N <- 236
    ia_timing <- 0.5

    models <- Mods(
      emax = c(0.5, 1, 2, 4), sigEmax = rbind(c(0.5, 3), c(1, 3), c(2, 3), c(4, 3)),
      quadratic = -0.1, doses = doses
    )

    oneSim <- sim_one_trial(
      times = times, bslMean = bslMean,
      errSD = errSD, rho = rho, maxEff = maxEff, RecrDist = RecrDist,
      LPFV.t = LPFV.t, models = models, ia_timing = ia_timing, N = N,
      doses = doses, alRatio = alRatio, delta_assumed = delta_assumed,
      alpha = alpha
    )

    scenRes <- run_scen(
      n_sim = 10, rho = 0.9, maxEff = 0.12,
      LPFV.t = 50, ia_timing = seq(0.3, 0.8, 0.05), delta_assumed = delta_assumed
    )

## References

Bornkamp, B., Zhou, J., Xi, D., and Cao, W. (2024), “Futility analyses for the MCP-mod methodology based on longitudinal models.”