---
title: "Memory mapping: disk-backed designs"
output:
  rmarkdown::html_vignette:
    toc: true
    css: vignette_styles.css
bibliography: references.bib
csl: ieee.csl
link-citations: true
vignette: >
  %\VignetteIndexEntry{Memory mapping: disk-backed designs}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.align = "center",
                      fig.width = 6, fig.height = 4)
options(width = 80)
library(TRexSelector)
old_threads <- trex_set_num_threads(min(2L, trex_get_max_threads()))
```

# A design larger than the memory

A memory-mapped matrix lives in a file on disk, and the operating system pages the parts in use into
memory. The C++ backend binds such a file exactly as it binds an R matrix, so every selector, every
T-solver, the scalers, the low-memory regression engines and the clustering accept one. That is how a
sparse regression problem with five million variables was solved on a laptop [@scheidt2023bigtrex]. The
file holds the matrix as raw doubles in column-major order, the layout of R and Eigen, without a header.

# Creating and opening a mapped matrix

`mem_matrix_from()` writes an R matrix to a file and returns a handle, `mem_matrix()` opens an existing
file with its dimensions and the access mode.

```{r create}
set.seed(5)
n <- 100
p <- 200
X <- matrix(rnorm(n * p), n, p)
path <- tempfile(fileext = ".bin")
mm <- mem_matrix_from(X, path)
class(mm)
dim(mm)
file.size(path) == 8 * n * p
```

The handle supports the matrix operations one needs to inspect and to edit the file. Reading a block
copies it into R memory, the backend never does.

```{r ops}
round(mm[1:2, 1:3], 3)
identical(mm[1:2, 1:3], X[1:2, 1:3])
mm[1, 1] <- 42
mm[1, 1]
mm[1, 1] <- X[1, 1]
max(abs(as.matrix(mm) - X))
ro <- mem_matrix(path, rows = n, cols = p, mode = "readonly")
tryCatch(ro[1, 1] <- 0, error = function(e) cat("error:", conditionMessage(e), "\n"))
```

# Streaming a design into the file

A design that does not fit in memory is written column by column. The file is raw doubles in
column-major order, so `writeBin()` of one column after another produces it.

```{r stream}
path2 <- tempfile(fileext = ".bin")
con <- file(path2, open = "wb")
for (j in seq_len(p)) {
  set.seed(1000 + j)
  writeBin(rnorm(n), con)
}
close(con)
big <- mem_matrix(path2, rows = n, cols = p, mode = "readwrite")
set.seed(1000 + 3)
max(abs(as.numeric(big[, 3]) - rnorm(n)))
```

# Selectors on a mapped design

The selectors take the handle where they take `X`. The file must be open in `"readwrite"` mode,
because the selector centers and scales the design in place and restores it when `select()` returns
(the page on working without data copies has the contract).

```{r select}
beta <- c(rep(1.5, 4), rep(0, p - 4))
y <- as.numeric(X %*% beta + rnorm(n))
sel_mem <- TRexSelector$new(X, y, tFDR = 0.1, seed = 3, verbose = FALSE,
                            control = trex_control(K = 10))
sel_mem$select()
sel_map <- TRexSelector$new(mm, y, tFDR = 0.1, seed = 3, verbose = FALSE,
                            control = trex_control(K = 10))
sel_map$select()
identical(sel_mem$selected_indices, sel_map$selected_indices)
max(abs(as.matrix(mm) - X))      # the file holds the original values again
```

The same holds for `TRexDASelector`, `TRexGVSSelector`, `TRexScreeningSelector`,
`TRexBiobankScreeningSelector`, `TRexSPCASelector` and `TRexTikhonovSelector`.
`trex_control(use_memory_mapping = TRUE)` additionally keeps the dummy matrices of the random
experiments in mapped files, for the case where $K \cdot L \cdot p$ dummy columns do not fit either.

# T-solvers and ML methods

A T-solver binds the mapped `X` as it binds a matrix, the scalers transform the file in place, the
low-memory engines of the regressions read it in place, and the clustering accepts it as `data`.

```{r solver}
D <- matrix(rnorm(n * p), n, p)
s <- TLARS_Solver$new(mm, D, y)
s$execute_step(T_stop = 2)
s$get_active_predictor_indices()
s$restore()
z <- MLZScoreScaler$new()
invisible(z$fit_transform_inplace(mm))
round(mean(mm[, 1]), 12)
invisible(z$inverse_transform_inplace(mm))
en <- MLElasticNet$new(low_memory = TRUE)
en$fit(mm, y, alpha = 1, n_lambda = 20)
dim(en$get_coef())
```

# When it pays

A mapped design pays when the design does not fit into memory, when several solvers share one design
and the working set should be loaded once, and when a design is produced by a streaming process and read
afterwards. For a design that fits comfortably in memory an R matrix is faster, since no paging happens.
Mapped files are never read into R's heap by the backend, so the memory footprint of a run is the
backend's working data, see `get_memory_bytes()` on the T-solvers.

```{r cleanup, include = FALSE}
rm(mm, ro, big, s)
invisible(gc())
unlink(c(path, path2))
trex_set_num_threads(old_threads)
```

# References
