# Data Generation: Mechanisms and Examples

# Quick start with `rmst.sim()`

For common settings — AFT or PH baseline, random censoring, and standard covariate distributions — `rmst.sim()` generates data from one function call. Use `covar_continuous()`, `covar_binary()`, and `covar_categorical()` to define covariates directly.

Code

    df <- rmst.sim(
      n              = 200,
      model          = "aft_lognormal",
      baseline       = list(mu = 2.5, sigma = 0.6),
      treat_effect   = -0.25,
      covariates     = list(
        covar_continuous("age",  mean = 62, sd = 10),
        covar_binary("sex",      p = 0.45),
        covar_continuous("x",    dist = "lognormal", meanlog = 0, sdlog = 0.5)
      ),
      target_censoring = 0.25,
      L    = 36,
      seed = 42
    )

    print(df)
    ── Simulated RMST Dataset ────────────────────────────────────
      Model          : AFT Log-Normal
      N (total)      : 200  |  Arm 0: 108  |  Arm 1: 92
      Events         : 150 (75.0%)
      Censored       : 50 (25.0%)
      Truncation time: 36  (stored as attribute)

    First 6 rows:
           time status arm      age sex         x
    1  7.731940      1   1 55.73546   1 1.5633592
    2 23.370037      1   1 63.83643   0 0.5923551
    3 16.726338      1   0 53.64371   1 2.6796031
    4  5.139254      0   1 77.95281   1 0.8254587
    5 11.223345      1   0 65.29508   1 2.2866152
    6  6.159279      1   0 53.79532   1 2.1299667

The result is a data frame of class `rmst_simdata`. Pass it directly to `rmst.power()` or `rmst.ss()`:

Code

    rmst.power(Surv(time, status) ~ age + sex + x,
               data = df, arm = "arm",
               sample_sizes = c(100, 200, 300), L = 36)

For designs requiring explicit covariate-dependent censoring or stratified assignment, use the full recipe system described below.

# What this covers

We show how to construct a recipe (covariates (X), treatment (A), event time (Y), and censoring (C)), and then simulate datasets with `simulate_from_recipe()`. We also show batch generation via `generate_recipe_sets()` and how to read compact metadata back with `load_recipe_sets()`.

The simulator takes a plain list as the recipe. No YAML is required.

## Recipe skeleton

Code

    list(
      n = 300,
      covariates = list(defs = list(/* see Covariates */)),
      treatment  = list(/* see Treatment */),
      event_time = list(/* see Event-time engines */),
      censoring  = list(/* see Censoring */),
      seed = 42
    )

# Covariates

Each covariate has a `name`, `type` (`"continuous"` or `"categorical"`), a `dist` with `params`, and optional `transform` steps applied after generation (e.g., `"center(a)"`, `"scale(b)"`).

Available distributions

Type | `dist` (string) | Parameters
--- | --- | ---
continuous | `normal` | `mean`, `sd`
continuous | `lognormal` | `meanlog`, `sdlog`
continuous | `gamma` | `shape`, `scale`
continuous | `weibull` | `shape`, `scale`
continuous | `uniform` | `min`, `max`
continuous | `beta` | `shape1`, `shape2`
continuous | `t` | `df`
categorical | `bernoulli` | `p` (probability of 1)
categorical | `categorical` | `prob = c(...)`, `labels = c(...)` (opt.)
categorical | `ordinal` | `prob = c(...)`, `labels = c(...)` (opt.)

Example: define covariates

Code

    covs <- list(
      list(name="age",   type="continuous",  dist="normal",     params=list(mean=62, sd=10),
           transform=c("center(60)","scale(10)")),
      list(name="sex",   type="categorical", dist="bernoulli",  params=list(p=0.45)),
      list(name="stage", type="categorical", dist="ordinal",
           params=list(prob=c(0.3,0.5,0.2), labels=c("I","II","III"))),
      list(name="x",     type="continuous",  dist="lognormal",  params=list(meanlog=0, sdlog=0.6))
    )

# Treatment

Choose one `assignment`:

Assignment | Key fields | Meaning
--- | --- | ---
`"randomization"` | `allocation = "a:b"` | Bernoulli with probability (p_1 = a/(a+b)).
`"stratified"` | `allocation`, `stratify_by = c(...)` | Same allocation within each stratum defined by listed categorical covariates.
`"logistic_ps"` | `ps_model = list(formula, beta)` | Treatment probability is `plogis(eta)` from the user-specified model.

Examples

Randomization:

Code

    tr_rand <- list(assignment="randomization", allocation="1:1")

Stratified by `"stage"`:

Code

    tr_strat <- list(assignment="stratified", allocation="2:1", stratify_by=c("stage"))

Logistic propensity:

Code

    tr_ps <- list(
      assignment = "logistic_ps",
      ps_model  = list(
        formula = "~ 1 + x + sex",
        beta    = c(-0.3, 1.2, -0.6)  # (Intercept), x, sex
      )
    )

# Event-time engines

Let `Z` be treatment (0/1), `X` be covariates, and `eta` be the linear predictor (defined in `effects`, below). Supported engines and baseline parameterizations:

Model (user-facing) | `model` value | Baseline parameters | Notes
--- | --- | --- | ---
AFT Lognormal | `"aft_lognormal"` | `mu`, `sigma` | `log(T) = mu + eta + sigma * epsilon`, with `epsilon ~ N(0, 1)`.
AFT Weibull | `"aft_weibull"` | `shape`, `scale` | Baseline survival `S0(t) = exp(-(t/scale)^shape)` with an AFT shift via `eta`.
AFT Log-Logistic | `"aft_loglogistic"` | `shape`, `scale` | Log-logistic AFT model with `T = scale * exp(eta) * (U/(1-U))^(1/shape)`.
PH Exponential | `"ph_pwexp"` | `rates = c(rate)`, `cuts = numeric(0)` | Piecewise exponential with a single segment is exponential.
PH Weibull | `"ph_weibull"` | `shape`, `scale` | Proportional hazards with a Weibull baseline.
PH Gompertz | `"ph_gompertz"` | `rate`, `gamma` | Hazard `h(t) = rate * exp(gamma * t)`.
PH Piecewise Exp | `"ph_pwexp"` | `rates = c(r1,r2,...)`, `cuts = c(...)` | Segment-specific hazard is `r_s * exp(eta)`.

Effects and linear predictor

Specify effects on the appropriate scale (AFT: log-time; PH: log-hazard):

Code

    effects = list(
      intercept  = 0,                      # default is 0
      treatment  = -0.25,
      covariates = list(age = 0.01, sex = -0.2)
      # or: formula="~ age + sex", beta=c(0.01, -0.2)
    )

> `effects$covariates` must be a named list (e.g., `list(age=0.01)`), not a named vector from `c()`.

# Censoring

We assume a single censoring mechanism that may depend on observed covariates (but not on the unobserved event time after conditioning on those covariates). Two user-facing modes are supported:

Mode | Fields | Semantics
--- | --- | ---
`"target_overall"` | `target`, `admin_time` | Finds an exponential random-censoring rate so the overall censoring fraction is approximately `target`, with an administrative floor at `admin_time` (if provided).
`"explicit"` | Any of: `administrative = list(time=...)`; `random = list(dist="exponential", params=list(rate=...))`; `dependent = list(formula="~ ...", base=..., beta=c(...))` | Compose administrative, random, and covariate-dependent censoring directly. The `dependent` block defines a covariate-dependent log-rate model; it does not denote competing risks.

> Note. No cause-specific or competing-risk censoring is generated here; the simulator’s `dependent` option is strictly covariate-dependent censoring under a single mechanism.

## Examples

### Target overall censoring

Code

    cz_target <- list(
      mode        = "target_overall",
      target      = 0.25,   # desired overall censoring fraction
      admin_time  = 36      # optional administrative cutoff (same units as event time)
    )

### Explicit mix (administrative + random)

Code

    cz_explicit <- list(
      mode = "explicit",
      administrative = list(time = 36),                                      # admin cutoff
      random         = list(dist = "exponential", params = list(rate = 0.02))# random censoring
    )

### Explicit covariate-dependent censoring

The dependent block uses a log-rate model: \[ \log \lambda_c(X) = \log(b) + X^\top \beta, \] where `b > 0` is the baseline rate. Implementation uses `base` as a rate, and `beta` on the log-rate scale. When `base` is supplied, the formula must not include an intercept.

Code

    cz_dep <- list(
      mode = "explicit",
      dependent = list(
        # sex is coded numeric 0/1 in these recipes; if yours is a factor, use I(as.numeric(sex)-1)
        formula = "~ 0 + age + sex",   # NO intercept because we use 'base' as the rate
        base    = 0.03,                # positive baseline rate
        beta    = c(0.012, 0.35)       # (age, sex) on log-rate scale
      ),
      administrative = list(time = 48) # optional admin cutoff
    )

> If you prefer a log-intercept style, omit `base` and include an intercept in the formula and in `beta`, e.g. `formula="~ 1 + age + sex"; beta=c(-3.5, 0.012, 0.35)`.

# Using a censoring recipe inside a full simulation

Code

    # Covariates (sex will be numeric 0/1 here)
    covs <- list(
      list(name="age", type="continuous",  dist="normal",    params=list(mean=62, sd=10)),
      list(name="sex", type="categorical", dist="bernoulli", params=list(p=0.45))
    )

    # Censoring: single covariate-dependent mechanism using a positive base rate
    cz_dep <- list(
      mode = "explicit",
      dependent = list(
        formula = "~ 0 + age + sex",  # NO intercept; sex is numeric 0/1
        base    = 0.03,               # baseline censoring rate (>0)
        beta    = c(0.012, 0.35)      # (age, sex) on log-rate scale
      ),
      administrative = list(time = 48)
    )

    # Quick sanity check (optional)
    mm   <- model.matrix(~ 0 + age + sex, data.frame(age=c(50,70), sex=c(0,1)))
    rate <- cz_dep$dependent$base * exp(mm %*% matrix(cz_dep$dependent$beta, ncol=1))
    stopifnot(all(rate > 0 & is.finite(rate)))

    # Recipe
    rec <- list(
      n = 300,
      covariates = list(defs = covs),
      treatment  = list(assignment="randomization", allocation="1:1"),
      event_time = list(
        model    = "ph_pwexp",
        baseline = list(rates = c(0.05), cuts = numeric(0)), # exponential baseline
        effects  = list(intercept=0, treatment=-0.3, covariates=list(age=0.01, sex=-0.2))
      ),
      censoring  = cz_dep,
      seed = 2025
    )

    # Simulate
    dat <- simulate_from_recipe(validate_recipe(rec))

    # Output
    knitr::kable(
      data.frame(achieved_censoring = attr(dat, "achieved_censoring")),
      caption = "Achieved censoring rate"
    )

Achieved censoring rate
achieved_censoring
0.4666667

Code

    knitr::kable(utils::head(dat, 8), caption = "First 8 rows of simulated data")

First 8 rows of simulated data
time | status | arm | age | sex
--- | --- | --- | --- | ---
18.7861607 | 1 | 0 | 47.80127 | 0
28.4958510 | 1 | 0 | 52.92040 | 0
17.4147421 | 0 | 0 | 70.47633 | 0
2.1688157 | 1 | 1 | 66.99463 | 1
1.9444436 | 0 | 0 | 55.62174 | 0
0.0312674 | 0 | 1 | 86.15505 | 1
31.0842017 | 1 | 1 | 76.80908 | 1
0.3933869 | 0 | 1 | 54.61018 | 1

# Worked examples

We now build full recipes and call `simulate_from_recipe()`. We report realized censoring via `attr(dat, "achieved_censoring")`.

## Example 1 — AFT Lognormal

Code

    # Covariates
    covs1 <- list(
      list(name="age",   type="continuous",  dist="normal",     params=list(mean=62, sd=10),
           transform=c("center(60)","scale(10)")),
      list(name="sex",   type="categorical", dist="bernoulli",  params=list(p=0.45)),
      list(name="stage", type="categorical", dist="ordinal",
           params=list(prob=c(0.3,0.5,0.2), labels=c("I","II","III"))),
      list(name="x",     type="continuous",  dist="lognormal",  params=list(meanlog=0, sdlog=0.6))
    )

    # Recipe
    rec1 <- list(
      n = 300,
      covariates = list(defs = covs1),
      treatment  = list(assignment="randomization", allocation="1:1"),
      event_time = list(model="aft_lognormal",
                        baseline=list(mu=3.0, sigma=0.6),
                        effects=list(intercept=0, treatment=-0.25,
                                     covariates=list(age=0.01, sex=-0.2, x=0.05))),
      censoring  = list(mode="target_overall", target=0.25, admin_time=36),
      seed = 11
    )

    # Simulate
    dat1 <- simulate_from_recipe(validate_recipe(rec1))

    # Display: data (first rows)
    knitr::kable(utils::head(dat1, 8), caption = "Example 1 — AFT Lognormal: first 8 rows")

Example 1 — AFT Lognormal: first 8 rows
time | status | arm | age | sex | stage | x
--- | --- | --- | --- | --- | --- | ---
22.584990 | 1 | 1 | -0.9484864 | 0 | II | 1.0093289
17.658806 | 1 | 1 | 0.6443458 | 1 | II | 1.0588519
20.540033 | 1 | 1 | 0.2119294 | 0 | II | 1.8124911
7.427722 | 1 | 1 | 0.1907200 | 0 | II | 1.2929821
5.884245 | 1 | 1 | -0.1023776 | 1 | III | 1.9560519
16.663357 | 1 | 0 | 0.6923550 | 1 | III | 0.6415970
18.282139 | 1 | 1 | -0.4027196 | 1 | II | 0.2898592
25.896230 | 0 | 1 | -0.4822765 | 0 | II | 0.3896278

Code

    # Display: key attributes (compact)
    attr_tbl1 <- data.frame(
      attribute = c("achieved_censoring", "n_rows", "n_cols", "class"),
      value = c(
        as.character(attr(dat1, "achieved_censoring")),
        nrow(dat1),
        ncol(dat1),
        paste(class(dat1), collapse = ", ")
      )
    )
    knitr::kable(attr_tbl1, caption = "Example 1 — Attributes")

Example 1 — Attributes
attribute | value
--- | ---
achieved_censoring | 0.216666666666667
n_rows | 300
n_cols | 7
class | data.frame

## Example 2 — AFT Weibull

Code

    # Start from Example 1 and tweak the event-time engine
    rec2 <- rec1
    rec2$event_time <- list(
      model    = "aft_weibull",
      baseline = list(shape=1.3, scale=12),
      effects  = list(intercept=0, treatment=-0.20, covariates=list(age=0.008, x=0.04))
    )

    dat2 <- simulate_from_recipe(validate_recipe(rec2), seed=12)

    # Display: data (first rows)
    knitr::kable(utils::head(dat2, 8), caption = "Example 2 — AFT Weibull: first 8 rows")

Example 2 — AFT Weibull: first 8 rows
time | status | arm | age | sex | stage | x
--- | --- | --- | --- | --- | --- | ---
0.8767022 | 1 | 1 | 0.1684122 | 0 | I | 1.2786805
3.7917670 | 0 | 0 | -0.3638602 | 0 | III | 0.6734713
5.3436817 | 1 | 1 | -0.0695371 | 0 | II | 1.7285599
8.2146778 | 1 | 0 | -0.2834247 | 1 | II | 0.8830733
16.3483547 | 1 | 0 | 1.2408825 | 0 | III | 1.9996407
8.5485716 | 1 | 1 | 2.1120583 | 1 | I | 2.1527826
6.0849117 | 1 | 1 | 2.1253492 | 0 | II | 1.7435251
7.6357950 | 0 | 0 | 0.9624655 | 0 | II | 0.9642604

Code

    # Display: key attributes (compact)
    attr_tbl2 <- data.frame(
      attribute = c("achieved_censoring", "n_rows", "n_cols", "class"),
      value = c(
        as.character(attr(dat2, "achieved_censoring")),
        nrow(dat2),
        ncol(dat2),
        paste(class(dat2), collapse = ", ")
      )
    )
    knitr::kable(attr_tbl2, caption = "Example 2 — Attributes")

Example 2 — Attributes
attribute | value
--- | ---
achieved_censoring | 0.226666666666667
n_rows | 300
n_cols | 7
class | data.frame

## Example 3 — PH Exponential (single segment)

Code

    rec3 <- list(
      n = 400,
      covariates = list(defs = covs1),
      treatment  = list(assignment="randomization", allocation="1:1"),
      event_time = list(
        model    = "ph_pwexp",
        baseline = list(rates = c(0.05), cuts = numeric(0)),
        effects  = list(intercept=0, treatment=-0.3, covariates=list(age=0.01, x=0.03))
      ),
      censoring  = list(mode="target_overall", target=0.20, admin_time=30),
      seed = 13
    )

    dat3 <- simulate_from_recipe(validate_recipe(rec3))

    # Display: data (first rows)
    knitr::kable(utils::head(dat3, 8), caption = "Example 3 — PH Exponential: first 8 rows")

Example 3 — PH Exponential: first 8 rows
time | status | arm | age | sex | stage | x
--- | --- | --- | --- | --- | --- | ---
22.4112596 | 1 | 0 | 1.903383 | 0 | II | 1.0272998
1.2157570 | 1 | 1 | 1.551547 | 0 | II | 1.1600019
16.3664550 | 1 | 1 | -0.256743 | 1 | II | 1.0494973
30.0000000 | 0 | 1 | 1.923054 | 0 | II | 0.9904563
7.2081849 | 1 | 1 | -1.782442 | 1 | III | 0.5716505
24.0173726 | 1 | 0 | 1.696357 | 0 | III | 0.5826786
0.6726071 | 1 | 1 | 1.067309 | 1 | I | 1.9916004
2.9351145 | 1 | 0 | -0.084403 | 1 | II | 1.6589311

Code

    # Display: a small summary + attributes
    summ3 <- data.frame(
      Metric = c("n", "Events", "Censoring rate", "Mean time", "Median time"),
      Value  = c(nrow(dat3),
                 sum(dat3$status == 1, na.rm = TRUE),
                 round(mean(dat3$status == 0, na.rm = TRUE), 3),
                 round(mean(dat3$time,   na.rm = TRUE), 2),
                 round(stats::median(dat3$time, na.rm = TRUE), 2))
    )
    knitr::kable(summ3, caption = "Example 3 — Summary")

Example 3 — Summary
Metric | Value
--- | ---
n | 400.000
Events | 289.000
Censoring rate | 0.278
Mean time | 16.340
Median time | 14.910

Code

    attr_tbl3 <- data.frame(
      attribute = c("achieved_censoring", "n_rows", "n_cols", "class"),
      value = c(
        as.character(attr(dat3, "achieved_censoring")),
        nrow(dat3),
        ncol(dat3),
        paste(class(dat3), collapse = ", ")
      )
    )
    knitr::kable(attr_tbl3, caption = "Example 3 — Attributes")

Example 3 — Attributes
attribute | value
--- | ---
achieved_censoring | 0.2775
n_rows | 400
n_cols | 7
class | data.frame

## Example 4 — PH Piecewise Exponential (multi-segment)

Code

    rec4 <- list(
      n = 500,
      covariates = list(defs = list(
        list(name="age", type="continuous",  dist="normal",    params=list(mean=60, sd=8)),
        list(name="sex", type="categorical", dist="bernoulli", params=list(p=0.5)),
        list(name="x",   type="continuous",  dist="lognormal", params=list(meanlog=0, sdlog=0.5))
      )),
      treatment  = list(assignment="randomization", allocation="1:1"),
      event_time = list(
        model    = "ph_pwexp",
        baseline = list(rates=c(0.10, 0.06, 0.03), cuts=c(6, 18)),
        effects  = list(intercept=0, treatment=-0.4, covariates=list(age=0.01, x=0.03))
      ),
      censoring  = list(mode="target_overall", target=0.25, admin_time=30),
      seed = 123
    )

    dat4 <- simulate_from_recipe(validate_recipe(rec4))

    # Display: data (first rows)
    knitr::kable(utils::head(dat4, 8), caption = "Example 4 — PH Piecewise Exponential (multi-segment): first 8 rows")

Example 4 — PH Piecewise Exponential (multi-segment): first 8 rows
time | status | arm | age | sex | x
--- | --- | --- | --- | --- | ---
1.8844656 | 1 | 1 | 62.78288 | 0 | 1.1852957
0.1803433 | 1 | 0 | 52.52264 | 0 | 0.4197519
30.0000000 | 0 | 0 | 72.74434 | 0 | 2.1693298
21.2203789 | 0 | 0 | 72.03558 | 0 | 2.9706366
3.1738917 | 1 | 0 | 54.10477 | 0 | 1.3775195
2.4408164 | 1 | 0 | 59.40696 | 0 | 0.3945189
2.1540147 | 1 | 0 | 64.18567 | 1 | 0.8663525
1.9268445 | 0 | 1 | 66.97149 | 1 | 0.7599901

Code

    # Display: summary + attributes
    summ4 <- data.frame(
      Metric = c("n", "Events", "Censoring rate", "Mean time", "Median time"),
      Value  = c(nrow(dat4),
                 sum(dat4$status == 1, na.rm = TRUE),
                 round(mean(dat4$status == 0, na.rm = TRUE), 3),
                 round(mean(dat4$time,   na.rm = TRUE), 2),
                 round(stats::median(dat4$time, na.rm = TRUE), 2))
    )
    knitr::kable(summ4, caption = "Example 4 — Summary")

Example 4 — Summary
Metric | Value
--- | ---
n | 500.00
Events | 370.00
Censoring rate | 0.26
Mean time | 6.65
Median time | 4.02

Code

    attr_tbl4 <- data.frame(
      attribute = c("achieved_censoring", "n_rows", "n_cols", "class"),
      value = c(
        as.character(attr(dat4, "achieved_censoring")),
        nrow(dat4),
        ncol(dat4),
        paste(class(dat4), collapse = ", ")
      )
    )
    knitr::kable(attr_tbl4, caption = "Example 4 — Attributes")

Example 4 — Attributes
attribute | value
--- | ---
achieved_censoring | 0.26
n_rows | 500
n_cols | 6
class | data.frame

# Generate data based on formula (event & censoring)

This example uses `~` formulas for both event-time covariate effects and covariate-dependent censoring. We keep the treatment effect as a separate scalar to avoid design ambiguity.

Code

    # Covariates (sex numeric 0/1)
    covs_f <- list(
      list(name="age", type="continuous",  dist="normal",    params=list(mean=62, sd=10)),
      list(name="sex", type="categorical", dist="bernoulli", params=list(p=0.45))
    )

    trt_f <- list(assignment="randomization", allocation="1:1")

    # Event-time engine: AFT lognormal with formula-based covariate effects
    evt_f <- list(
      model    = "aft_lognormal",
      baseline = list(mu = 3.1, sigma = 0.55),
      effects  = list(
        treatment  = -0.25,                 # effect of treatment (log-time scale)
        formula    = "~ 0 + age + sex",     # covariate effects via formula (no intercept)
        beta       = c(0.010, -0.20)        # (age, sex) -- match order in formula
      )
    )

    # Censoring: covariate-dependent via log-rate model (single mechanism)
    cz_f <- list(
      mode = "explicit",
      dependent = list(
        formula = "~ 0 + age + sex",  # NO intercept; sex numeric 0/1
        base    = 0.03,               # positive rate
        beta    = c(0.012, 0.35)      # (age, sex) on log-rate scale
      ),
      administrative = list(time = 48)  # optional admin cutoff
    )

    # Full recipe
    rec_f <- list(
      n = 350,
      covariates = list(defs = covs_f),
      treatment  = trt_f,
      event_time = evt_f,
      censoring  = cz_f,
      seed = 777
    )

    # Simulate
    dat_f <- simulate_from_recipe(validate_recipe(rec_f))

    # Display
    knitr::kable(
      data.frame(achieved_censoring = attr(dat_f, "achieved_censoring")),
      caption = "Achieved censoring rate (formula-based example)"
    )

Achieved censoring rate (formula-based example)
achieved_censoring
0.8714286

Code

    knitr::kable(utils::head(dat_f, 8), caption = "First 8 rows — formula-based example")

First 8 rows — formula-based example
time | status | arm | age | sex
--- | --- | --- | --- | ---
0.7388634 | 0 | 1 | 49.95591 | 1
4.0901774 | 0 | 0 | 53.08751 | 1
7.8761496 | 0 | 1 | 62.85469 | 1
4.1360942 | 0 | 1 | 66.66486 | 0
8.6479773 | 0 | 0 | 65.48561 | 1
13.2841235 | 0 | 0 | 66.07921 | 0
6.5725020 | 0 | 1 | 71.87909 | 1
11.6268551 | 0 | 0 | 58.79355 | 1

# Batch generation with metadata

For simulation studies, write multiple scenarios and formats together. The writer creates a `manifest.rds` with a list-column `meta` describing each dataset. The loader reattaches attributes when reading back.

Code

    # Build a base recipe here (self-contained)
    base <- validate_recipe(list(
      n = 200,
      covariates = list(defs = list(
        list(name="age", type="continuous",  dist="normal", params=list(mean=62, sd=10)),
        list(name="sex", type="categorical", dist="bernoulli", params=list(p=0.45)),
        list(name="x",   type="continuous",  dist="lognormal", params=list(meanlog=0, sdlog=0.6))
      )),
      treatment  = list(assignment="randomization", allocation="1:1"),
      event_time = list(
        model    = "aft_weibull",
        baseline = list(shape=1.3, scale=12),
        effects  = list(intercept=0, treatment=-0.20, covariates=list(age=0.008, x=0.04))
      ),
      censoring  = list(mode="target_overall", target=0.25, admin_time=36),
      seed = 3026
    ))

    out_dir <- file.path(tempdir(), "rmstss-manifest-demo")
    unlink(out_dir, recursive = TRUE, force = TRUE)

    man <- generate_recipe_sets(
      base_recipe = base,
      vary = list(n = c(200, 400),
                  "event_time.effects.treatment" = c(-0.15, -0.25)),
      out_dir  = out_dir,
      formats  = c("rds","csv"),
      n_reps   = 1,
      seed_base = 2025
    )

    # Inspect the first row's compact metadata (fields only; no file paths)
    m <- readRDS(file.path(out_dir, "manifest.rds"))
    names(m)
     [1] "scenario_id"                     "rep"
     [3] "seed"                            "achieved_censoring"
     [5] "n"                               "file_txt"
     [7] "file_csv"                        "file_rds"
     [9] "file_rdata"                      "p__n"
    [11] "p__event_time.effects.treatment" "meta"
    if ("meta" %in% names(m) && length(m$meta[[1]]) > 0) {
      list(model = m$meta[[1]]$model,
           baseline = m$meta[[1]]$baseline,
           effects = m$meta[[1]]$effects,
           achieved_censoring = m$meta[[1]]$achieved_censoring,
           n = m$meta[[1]]$n)
    } else {
      "Manifest is minimal (older run); use rebuild_manifest() to enrich."
    }
    $model
    [1] "aft_weibull"

    $baseline
    $baseline$shape
    [1] 1.3

    $baseline$scale
    [1] 12

    $effects
    $effects$intercept
    [1] 0

    $effects$treatment
    [1] -0.15

    $effects$covariates
    $effects$covariates$age
    [1] 0.008

    $effects$covariates$x
    [1] 0.04

    $achieved_censoring
    [1] 0.275

    $n
    [1] 200

    # Load datasets back
    sets <- load_recipe_sets(file.path(out_dir, "manifest.rds"))
    attr(sets[[1]]$data, "achieved_censoring")
    [1] 0.275
    str(sets[[1]]$meta)
    List of 19
     $ dataset_id        : chr "sc001_r01"
     $ scenario_id       : int 1
     $ rep               : int 1
     $ seed_used         : int 3026
     $ n                 : int 200
     $ n_treat           : int 98
     $ n_control         : int 102
     $ event_rate        : num 0.725
     $ achieved_censoring: num 0.275
     $ model             : chr "aft_weibull"
     $ baseline          :List of 2
      ..$ shape: num 1.3
      ..$ scale: num 12
     $ effects           :List of 3
      ..$ intercept : num 0
      ..$ treatment : num -0.15
      ..$ covariates:List of 2
      .. ..$ age: num 0.008
      .. ..$ x  : num 0.04
     $ treatment         :List of 2
      ..$ assignment: chr "randomization"
      ..$ allocation: chr "1:1"
     $ censoring         :List of 3
      ..$ mode      : chr "target_overall"
      ..$ target    : num 0.25
      ..$ admin_time: num 36
     $ covariates        :List of 3
      ..$ :List of 4
      .. ..$ name  : chr "age"
      .. ..$ type  : chr "continuous"
      .. ..$ dist  : chr "normal"
      .. ..$ params:List of 2
      .. .. ..$ mean: num 62
      .. .. ..$ sd  : num 10
      ..$ :List of 4
      .. ..$ name  : chr "sex"
      .. ..$ type  : chr "categorical"
      .. ..$ dist  : chr "bernoulli"
      .. ..$ params:List of 1
      .. .. ..$ p: num 0.45
      ..$ :List of 4
      .. ..$ name  : chr "x"
      .. ..$ type  : chr "continuous"
      .. ..$ dist  : chr "lognormal"
      .. ..$ params:List of 2
      .. .. ..$ meanlog: num 0
      .. .. ..$ sdlog  : num 0.6
     $ allocation        : chr "1:1"
     $ params            :List of 2
      ..$ n                           : num 200
      ..$ event_time.effects.treatment: num -0.15
     $ files             :List of 4
      ..$ txt  : chr NA
      ..$ csv  : chr "C:\\Users\\arnab\\AppData\\Local\\Temp\\RtmpkrcmkA/rmstss-manifest-demo/sc1_r1.csv"
      ..$ rds  : chr "C:\\Users\\arnab\\AppData\\Local\\Temp\\RtmpkrcmkA/rmstss-manifest-demo/sc1_r1.rds"
      ..$ rdata: chr NA
     $ created_at        : chr "2026-04-26 15:51:24.433331"

# Reproducibility tips

Set `seed` in the recipe or pass `seed=` to `simulate_from_recipe()`.

> Assumptions. The censoring hazard depends on observed covariates via a user-specified single mechanism. There are no competing risks. Datasets contain only `time`, `status`, treatment, and covariates - no dependent-censoring indicators.