---
title: "Getting started with pmsims"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting started with pmsims}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r}
#| label: setup
#| include: false
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 6, fig.height = 4,
  warning = FALSE,
  message = FALSE
)
```

## What pmsims does

**pmsims** estimates the **minimum sample size** needed to develop a
prediction model to achieve a target level of performance **with
assurance**. Rather than relying on simple rules of thumb or closed‑form
formulae, pmsims uses **simulation** to:

-   Generate synthetic datasets that reflect your target setting
    (outcome type, prevalence or $R^2$, signal vs. noise predictors,
    and how complex the underlying signal is);
-   Fit a specified **model** (e.g., logistic regression or linear
    regression);
-   Evaluate a chosen **performance metric** (e.g., calibration slope,
    AUC); and
-   Trace a **learning curve** of performance as the training size
    increases.

```{=html}
<figure class="pmsims-workflow-figure">
<svg class="pmsims-workflow" viewBox="0 0 860 290" role="img"
     aria-label="The pmsims workflow: the user's inputs define a data function, model function and metric function. The simulation engine uses these to fit a learning curve and returns the minimum sample size, min_n.">
  <style>
    .pmsims-workflow {
      --wf-card: var(--bs-tertiary-bg, #f8f9fa);
      --wf-border: var(--bs-border-color, #dee2e6);
      --wf-text: var(--bs-body-color, #373a3c);
      --wf-muted: var(--bs-secondary-color, #6c757d);
      --wf-line: #8a959e;
      --wf-teal: #067f8d;          /* site link colour; white text passes AA */
      --wf-data: #04adbf;          /* site primary */
      --wf-model: #4682b4;         /* steelblue, as in plot() */
      --wf-metric: #e08a1e;        /* darkorange family, as in plot() */
      --wf-panel: #fff;
      --wf-curve: #4682b4;
      width: 100%; height: auto; display: block;
      font-family: "Source Sans Pro", "Source Sans 3", system-ui, sans-serif;
    }
    [data-bs-theme="dark"] .pmsims-workflow {
      --wf-line: #8f9aa3;
      --wf-teal: #0b8f9e;
      --wf-panel: #1b1f23;
      --wf-curve: #79aee0;
    }
    .pmsims-workflow .wf-box { fill: var(--wf-card); stroke: var(--wf-border); stroke-width: 1; }
    .pmsims-workflow .wf-label { font-size: 20px; font-weight: 600; fill: var(--wf-text); text-anchor: middle; }
    .pmsims-workflow .wf-code { font-family: "Source Code Pro", ui-monospace, monospace; font-size: 13px;
                             fill: var(--wf-muted); text-anchor: middle; }
    .pmsims-workflow .wf-tint { fill: color-mix(in srgb, var(--accent) 16%, var(--bs-body-bg, #fff));
                             stroke: var(--accent); stroke-width: 1.5; }
    .pmsims-workflow .wf-wire { fill: none; stroke: var(--wf-line); stroke-width: 2; stroke-linejoin: round; }
  </style>
  <defs>
    <marker id="pmsims-wf-head" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="5" markerHeight="5" markerUnits="strokeWidth" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="var(--wf-line)"/>
    </marker>
  </defs>
  <!-- User inputs -->
  <rect class="wf-box" x="8" y="111" width="146" height="68" rx="8" stroke-dasharray="5 4"/>
  <text class="wf-label" x="81" y="142">User inputs</text>
  <text class="wf-code"  x="81" y="164">simulate_*()</text>
  <!-- Wires: inputs to components -->
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M154,145 H182 V50 H209"/>
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M154,145 H209"/>
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M154,145 H182 V240 H209"/>
  <!-- Components: neutral card, coloured accent bar, function argument below -->
  <g>
    <rect class="wf-box wf-tint" style="--accent: var(--wf-data)" x="210" y="16" width="214" height="68" rx="8"/>
    <text class="wf-label" x="318" y="46">Data generator</text>
    <text class="wf-code"  x="318" y="68">data_function</text>
  </g>
  <g>
    <rect class="wf-box wf-tint" style="--accent: var(--wf-model)" x="210" y="111" width="214" height="68" rx="8"/>
    <text class="wf-label" x="318" y="141">Prediction model</text>
    <text class="wf-code"  x="318" y="163">model_function</text>
  </g>
  <g>
    <rect class="wf-box wf-tint" style="--accent: var(--wf-metric)" x="210" y="206" width="214" height="68" rx="8"/>
    <text class="wf-label" x="318" y="236">Metrics</text>
    <text class="wf-code"  x="318" y="258">metric_function</text>
  </g>
  <!-- Wires: components to engine -->
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M424,50 H452 V122 H483"/>
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M424,145 H483"/>
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M424,240 H452 V168 H483"/>
  <!-- Simulation engine: header band in site teal, panel styled like plot() -->
  <g>
    <rect class="wf-box" x="484" y="40" width="226" height="210" rx="8"/>
    <path d="M484,48 a8,8 0 0 1 8,-8 h210 a8,8 0 0 1 8,8 v28 h-226z" fill="var(--wf-teal)"/>
    <text class="wf-label" x="597" y="65" style="fill:#fff">Simulation engine</text>
    <rect x="498" y="90" width="198" height="146" rx="3" fill="var(--wf-panel)" stroke="var(--wf-border)"/>
    <!-- interval around min_n -->
    <rect x="584" y="91" width="30" height="144" fill="var(--wf-curve)" opacity=".14"/>
    <!-- target -->
    <line x1="499" y1="146" x2="695" y2="146" stroke="var(--wf-muted)" stroke-width="1.5" stroke-dasharray="5 4"/>
    <!-- min_n -->
    <line x1="598" y1="91" x2="598" y2="235" stroke="var(--wf-curve)" stroke-width="1.5"/>
    <!-- curve -->
    <path d="M510,222 C560,174 600,139 684,120" fill="none" stroke="var(--wf-curve)" stroke-width="2.5" stroke-linecap="round"/>
    <!-- replicates (point size = number of replicates) -->
    <g fill="var(--wf-panel)" stroke="var(--wf-text)" stroke-width="1.3">
      <circle cx="516" cy="214" r="2.5"/>
      <circle cx="542" cy="199" r="3"/>
      <circle cx="572" cy="166" r="3.5"/>
      <circle cx="596" cy="149" r="5"/>
      <circle cx="612" cy="139" r="4.5"/>
      <circle cx="654" cy="124" r="3"/>
    </g>
  </g>
  <!-- Output -->
  <path class="wf-wire" marker-end="url(#pmsims-wf-head)" d="M710,145 H755"/>
  <rect x="756" y="122" width="96" height="46" rx="23" fill="var(--wf-teal)"/>
  <text x="804" y="152" text-anchor="middle" fill="#fff" font-size="24" font-weight="600" font-style="italic">N<tspan dy="-7" font-size="16">*</tspan></text>
  <text class="wf-code" x="804" y="192">min_n</text>
</svg>
</figure>
```

The recommended design objective is **assurance**: the **smallest** $n$
such that a high proportion of repeated studies (e.g., 80%) meet the
target performance. In pmsims, this is implemented via the **20th
percentile** of the simulated performance distribution at each $n$.

## Required inputs at a glance

There are three wrapper functions for binary, continuous, and survival
outcomes, respectively:

- `simulate_binary()`
- `simulate_continuous()`
- `simulate_survival()`

All three take the same core inputs:

| Argument | Default | Description |
|------|------|--------------|
| `signal_parameters` | required | Number of predictors associated with the outcome |
| `noise_parameters` | `0` | Number of predictors unrelated to the outcome |
| `complexity` | `1` | Signal structure: `1` linear, `2` adds quadratic terms, `3` adds an interaction, `4` the Friedman function (requires `signal_parameters = 5`) |
| `data_control` | `NULL` | Correlation, distribution and nonlinear strength of the predictors (see `simulate_binary()`) |
| `model` | `"glm"`, `"lm"` or `"coxph"` | Model fitted to each simulated dataset (the penalised and machine-learning options are experimental) |
| `metric` | `"calibration_slope"` | Performance metric: `"calibration_slope"`, `"auc"`, `"r2"`, `"cindex"` or `"csse"` |
| `target_performance` | required | Minimum acceptable performance |
| `n_reps_total` | `1000` | Total number of simulation replicates |
| `mean_or_assurance` | `"assurance"` | Criterion used to choose the sample size |

The remaining inputs depend on the outcome:

| Function | Outcome-specific inputs | Models |
|------|--------------|----------|
| `simulate_binary()` | `maximum_achievable_cstatistic`, `outcome_prevalence` | `"glm"`, `"lasso"`, `"ridge"`, `"rf"`, `"xgboost"` |
| `simulate_continuous()` | `maximum_achievable_rsquared` | `"lm"`, `"lasso"`, `"ridge"`, `"rf"`, `"xgboost"` |
| `simulate_survival()` | `maximum_achievable_cindex`, `censoring_rate`, `baseline_hazard` (optional; default `1`) | `"coxph"`, `"lasso"`, `"ridge"`, `"rf"`, `"xgboost"` |

> Notes:
>
> - `maximum_achievable_*` represents the best plausible performance with
>   effectively unlimited data and calibrates the data generator.
> - `target_performance` is the minimum acceptable performance threshold used
>   to determine the required sample size.
> - `complexity` and `data_control` describe the data-generating mechanism, not
>   the model you plan to fit. The same configuration is used both to calibrate
>   the generator against `maximum_achievable_*` and to simulate the data, so a
>   more complex signal generally implies a larger required sample size.
> - For reproducibility, set a random seed (`set.seed()`).

## Installation

Install the stable release from [CRAN](https://CRAN.R-project.org/package=pmsims):

```{r}
# install.packages("pmsims")
library(pmsims)
```

## Binary-outcome example

We target the smallest *n* that meets the **assurance** criterion.

```{r}
#| echo: true
#| eval: false
set.seed(123)

binary_example <- simulate_binary(
  signal_parameters = 20,
  noise_parameters  = 0,
  complexity = 1,
  data_control = list(correlation = 0.3),
  outcome_prevalence = 0.30,
  maximum_achievable_cstatistic = 0.80,
  model = "glm",
  metric = "calibration_slope",
  target_performance = 0.85,
  n_reps_total = 1000,
  mean_or_assurance = "assurance"
)

binary_example
```

```{r Run binary}
#| echo: false
# `binary_example` is the result of the call above, shipped with the package.
print(binary_example)
```

The printed summary is a human-readable report. Implementation detail --- the
internal metric identifiers, the engine settings used for the search, and any
quantities recorded on an internal search scale --- is available through
`summary(binary_example)` or, equivalently,
`print(binary_example, verbose = TRUE)`.

Plot the estimated learning curve and identified sample size:

```{r}
#| eval: false
plot(binary_example)
```

The animation below shows the same search as it ran. With
`live_plot = TRUE`, the plot is redrawn after each batch of 20 replicates.
The final frame is what `plot(binary_example)` draws: the 20th percentile of
the calibration slope at each simulated sample size (point size shows the
number of replicates, with ±2 standard errors), the fitted learning curve,
the target (dashed line), the minimum sample size (solid line) with its
interval (shaded), and a shape-free cross-check (dotted line).

<p align="center">
  <video src="images/live_plot.mp4" autoplay loop muted playsinline width="100%" aria-label="Animation of the learning-curve search for the binary example. Each frame adds a batch of replicates; points accumulate around the target, the fitted curve settles, and the estimate converges on a minimum sample size of 1,000 (interval 972 to 1,061)."></video>
</p>

## How the search works

By default (`method = "curve"`), `pmsims` fits a learning curve,
$C(n) = a - b n^{-c}$, to the criterion at every sample size simulated so
far. A pilot doubles or halves $n$ until the criterion has been seen on both
sides of the target. Each further batch is placed where the fitted curve
crosses the target, and the answer is where the final curve crosses it. The
answer is then simulated again with fresh replicates to check that it meets
the target.

Every result has a `status`. `"ok"` means a sample size was found and the
check confirmed it. Otherwise `min_n` may be `NA`, and `status_message`
explains why (for example, `"unreachable"` when the curve levels off below
the target). The previous Gaussian-process engine is available with
`method = "mlpwr"`; see `?simulate_custom` for details of both engines.

## Continuous-outcome example

```{r}
#| echo: true
#| eval: false
continuous_example <- simulate_continuous(
  signal_parameters = 15,
  noise_parameters = 0,
  complexity = 1,
  data_control = list(correlation = 0.3),
  maximum_achievable_rsquared = 0.50,
  model = "lm",
  metric = "calibration_slope",
  target_performance = 0.95,
  n_reps_total = 1000,
  mean_or_assurance = "assurance"
)

continuous_example
```

```{r}
#| echo: false
# `continuous_example` is the result of the call above, shipped with the package.
print(continuous_example)
```

```{r, fig.alt="Plot showing learning curve for continuous outcome"}
plot(continuous_example)
```
