﻿---
title: "Checkpoint and resume long fits"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Checkpoint and resume long fits}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
set.seed(20260716)
```

```{r cran-gate, include = FALSE}
# The model fits below are evaluated when the article is built locally, in CI
# and for the pkgdown site (all of which set NOT_CRAN). CRAN's check farm gives
# the whole check a ten-minute budget, which these fits do not fit inside, so
# there the code is shown without being run.
EVAL_FITS <- identical(Sys.getenv("NOT_CRAN"), "true")
knitr::opts_chunk$set(eval = EVAL_FITS)
```

```{r load, message = FALSE}
library(tulpa)
```

## Why checkpoint

A nested-Laplace fit is a loop over independent hyperparameter grid
cells; a multi-chain NUTS fit is a loop over independent chains. Both can
run for a long time, and a killed or rebooted run should not have to
start over. tulpa writes each finished unit to a content-addressed
append log: on resume it reloads the completed units and runs only the
rest, and a run interrupted mid-write is detected and re-run rather than
trusted.

## Turn it on

Every nested-Laplace fitter takes `control$checkpoint = list(path =,
resume =)`. `path` is the checkpoint file; `resume = TRUE` reloads any
completed cells from it, `resume = FALSE` (the default) starts fresh and
overwrites.

```{r fit}
# A small nested-Laplace fit over an ICAR field on a chain graph.
S <- 30L
W <- matrix(0, S, S)
for (i in 1:(S - 1)) W[i, i + 1] <- W[i + 1, i] <- 1
df <- data.frame(region = factor(seq_len(S)))
df$x <- as.integer(df$region) / 10 + rnorm(S, 0, 0.3)
df$y <- rbinom(S, 20, plogis(-0.4 + 0.5 * df$x))

ckpt <- tempfile(fileext = ".ckpt")
fit <- tulpa(y ~ x + spatial(region), data = df, family = "binomial",
             n_trials = rep(20L, S),
             spatial = spatial_car(W, level = "obs"),
             mode = "laplace",
             control = list(checkpoint = list(path = ckpt, resume = FALSE)))
coef(fit)
```

The grid cells are now on disk:

```{r size}
file.exists(ckpt) && file.info(ckpt)$size > 0
```

## Resume

A second call with the same data, settings, and grid plus `resume =
TRUE` reloads every completed cell instead of re-solving it -- so this
call returns essentially instantly and to the same result.

```{r resume}
fit2 <- tulpa(y ~ x + spatial(region), data = df, family = "binomial",
              n_trials = rep(20L, S),
              spatial = spatial_car(W, level = "obs"),
              mode = "laplace",
              control = list(checkpoint = list(path = ckpt, resume = TRUE)))
all.equal(coef(fit), coef(fit2))
```

```{r cleanup, include = FALSE}
unlink(ckpt)
```

## What the fingerprint protects

The checkpoint header carries a fingerprint of the data, settings, and
grid. Resuming onto a file written for a *different* fit errors rather
than silently mixing results, and a torn final record (a run killed
mid-write) is truncated and re-run. A multi-chain NUTS resume is
bit-for-bit identical to the uninterrupted run, because a chain is
deterministic in its seed and data.
