---
title: "Define Non-Time-to-Event Endpoints"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Define Non-Time-to-Event Endpoints}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  cache = TRUE,
  cache.path = 'cache/defineNonTimeToEventEndpoints/',
  comment = '#>',
  dpi = 300,
  out.width = '100%'
)
```

```{r setup, message = FALSE, echo=FALSE}
library(dplyr)
library(TrialSimulator)
set.seed(12345)
```

`TrialSimulator` provides a flexible framework for defining and simulating a variety of clinical trial endpoints by specifying the `type` parameter in `endpoint`. This vignette covers non-time-to-event (non-TTE) endpoints, demonstrating how they can be defined, integrated into trial arms, and analyzed at pre-specified milestones. For time-to-event endpoints, please refer to the vignette [Define Time-to-Event Endpoints in Clinical Trials](defineTimeToEventEndpoints.html). For longitudinal endpoints, please refer to the vignette [Define Longitudinal Endpoints in Clinical Trials](defineLongitudinalEndpoints.html)

This vignette demonstrates how to use the following key functions to define non-TTE endpoints. For the sake of completeness, we also demonstrates how to define arms and trial with the created endpoints

- `endpoint`: Creates one or more endpoints. It can also be used to define covariates, bio-markers, sub-group indicators, etc. 
- `arm`: Creates one or more arms
- `add_endpoints`: Add one or more endpoints to an arm
- `milestone`: Defines one or more milestones when data snapshots are needed for analysis


## Define endpoints with random number generators

Similar to time-to-event endpoints, non-TTE endpoints can be defined using any univariate random number generator that takes `n` (number of observations) as its first argument. The `stats` package provides a set of random number generators that can be assigned to `generator` in `endpoints`, where additional arguments required by `generator` can be passed through `...`. When creating non-TTE endpoints, the argument `type` should be set to `"non-tte"`, and the argument `readout` should be specified as a named numeric vector, indicating the time required for the endpoint to be available for analysis after patient enrollment.

In the example below, we define two types of endpoints:

- Continuous endpoint: Tumor size change from baseline (`cfb`), available after 6 months, assuming a normal distribution (`generator = rnorm`) with custom `mean` and `sd`. 

- Binary endpoint: Objective response rate (`orr`), available after 2 months, assuming a binomial distribution (`generator = rbinom`) with `size = 1` and custom `prob`. 

```{r lljalk}
## endpoints in placebo arm
tumor_cfb_pbo <- endpoint(name = 'cfb', type = 'non-tte', 
                          readout = c(cfb = 6),
                          generator = rnorm, mean = .8, sd = 3.2)
orr_pbo <- endpoint(name = 'orr', type = 'non-tte', 
                    readout = c(orr = 2),
                    generator = rbinom, size = 1, prob = .1)

## define the placebo arm
pbo <- arm(name = 'placebo')
pbo$add_endpoints(tumor_cfb_pbo, orr_pbo)

## endpoints in treatment arm
tumor_cfb_trt <- endpoint(name = 'cfb', type = 'non-tte', 
                          readout = c(cfb = 6), 
                          generator = rnorm, mean = -2.3, sd = 1.5)
orr_trt <- endpoint(name = 'orr', type = 'non-tte', 
                    readout = c(orr = 2), 
                    generator = rbinom, size = 1, prob = .25)

## define the treatment arm
trt <- arm(name = 'treatment')
trt$add_endpoints(tumor_cfb_trt, orr_trt)
```

### Baseline endpoints

Some variables are observed at randomization rather than after a delay, for example a baseline covariate, biomarker, or subgroup indicator. Such a variable is a non-TTE endpoint with a readout of `0`. Instead of writing `type = 'non-tte', readout = c(biomarker = 0)`, you can set `type = 'baseline'`, in which case the readout is `0` automatically and must be omitted from `readout`:

```{r baseline-ep}
## a baseline biomarker available at randomization
biomarker <- endpoint(name = 'biomarker', type = 'baseline',
                      generator = rbinom, size = 1, prob = .3)
```

`type = 'baseline'` is a convenience alias for a readout-`0` non-TTE endpoint; the two definitions below are equivalent. Specifying a `readout` for a `'baseline'` endpoint (even `0`) raises an error.

```{r baseline-equiv, eval = FALSE}
## these are equivalent
endpoint(name = 'biomarker', type = 'baseline',
         generator = rbinom, size = 1, prob = .3)
endpoint(name = 'biomarker', type = 'non-tte', readout = c(biomarker = 0),
         generator = rbinom, size = 1, prob = .3)
```

With the treatment arms defined, we can proceed to create a trial. Patients are recruited at a piecewise constant rate, with an accrual pattern as follows:

- First 6 months: 10 patients per month.
- After 6 months: 20 patients per month until 420 patients are randomized 1:1 into the two arms.

We also specify a dropout process with a Weibull distribution. The dropout rates are set as follows:

- 15% dropout at 12 months
- 30% dropout at 18 months

These constraints are resolved using the Weibull dropout function:

$$
\begin{split}
0.15 & = & 1 - e^{-(12/\lambda)^k} \\
0.30 & = & 1 - e^{-(18/\lambda)^k}
\end{split}
$$

```{r oheahs}
dropout_pars <- weibullDropout(c(12, 18), c(.15, .30))
dropout_pars
```

Using the computed scale parameter $\lambda=$ `r round(dropout_pars['scale'], 3)` and shape parameter $k=$ `r round(dropout_pars['shape'], 3)`, we specify the trial setup: 

```{r lihoehf}
accrual_rate <- data.frame(end_time = c(6, Inf), 
                           piecewise_rate = c(10, 20))

trial <- trial(
  name = 'Trial-31415', description = 'Example Clinical Trial', 
  n_patients = 420, duration = 30, 
  enroller = StaggeredRecruiter, accrual_rate = accrual_rate, 
  dropout = rweibull, scale = 30.636, shape = 1.939
)

## add arms to the trial
trial$add_arms(sample_ratio = c(1, 1), trt, pbo)
trial
```

Here `accrual_rate` is an argument of `TrialSimulator::StaggeredRecruiter` controlling how patients are recruited into the trial. Similarly, `scale` and `shape` are arguments of `rweibull`. All these arguments are passed through `...` of `trial()`. 

`TrialSimulator` allows defining trial milestones at specific time points when data is locked for analysis. Here, we define three key milestones:

1. Interim Analysis: Triggered when `orr` has been observed for 60 patients.
2. Random Checkpoint: For illustration purpose only. Triggered when the trial has reached at least 10 months, and at least one of the following conditions is met:
    - `cfb` has been observed for at least 100 patients, 
    - `orr` has been observed for at least 180 patients. 
3. Final Analysis: Occurs when the trial reaches 30 months.

```{r llieh}
interim <- milestone(name = 'interim', 
                     when = eventNumber(endpoint = 'orr', n = 60), 
                     action = doNothing)

random <- milestone(name = 'random', 
                    when = 
                      calendarTime(time = 10) & 
                      (eventNumber(endpoint = 'cfb', n = 100) | 
                         eventNumber(endpoint = 'orr', n = 180)
                       ), 
                    action = doNothing)

final <- milestone(name = 'final', 
                   when = calendarTime(time = 30), 
                   action = doNothing)
```

Here `action = doNothing` in `milestone` means we don't expect any action at the time of triggered milestones. In practice, instead of `doNothing`, custom action function can be adopted to add or remove arms (e.g., dose selection), adjust sample ratio per arm, or carry out statistical analysis based on locked data. These advanced setups are covered in other vignettes.

Next, we register the milestones with a listener and create a controller to monitor and execute the trial.

```{r ihhf}
## register milestones to the listener
listener <- listener()
listener$add_milestones(interim, random, final)

## run the trial
controller <- controller(trial, listener)
controller$run()
```

We can inspect the dataset locked at different milestone by calling member function `get_locked_data` with milestone names. Ideally, this should be done within custom action function, where decision is made based on data locked at the time of a milestone. 

```{r ihegea}
interim_data <- trial$get_locked_data(milestone_name = 'interim')
random_data <- trial$get_locked_data(milestone_name = 'random')
final_data <- trial$get_locked_data(milestone_name = 'final')
head(interim_data)
```

Since `cfb` has a 6-month readout time, at interim analysis, some patients’ `cfb` values are still unavailable, appearing as `NA` in `interim_data.` However, these values become available in `random_data` collected at a later time point. This demonstrates how `TrialSimulator` properly and automatically handles endpoint availability at different milestones

```{r eqfee}
not_ready_at_interim <- 
  interim_data %>% 
  dplyr::filter(is.na(cfb) & 
                  is.na(orr) & 
                  enroll_time + 6 < dropout_time) %>% 
  head() %>% 
  print()

random_data %>% 
  dplyr::filter(patient_id %in% not_ready_at_interim$patient_id) %>% 
  print()
```

In this example, we simulate tumor size change from baseline (`cfb`). However, in many trials, it is more appropriate to simulate tumor size at both baseline and follow-up separately to allow for more complex modeling, such as longitudinal or repeated measures analysis. This will be covered in another vignette.

With this flexible setup, `TrialSimulator` enables efficient endpoint definition, adaptive trial execution, and data monitoring—allowing users to design and simulate clinical trials tailored to specific research needs.
