---
title: "Multiplying Matrices in macpan2 Models"
output: 
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Multiplying Matrices in macpan2 Models}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
editor_options: 
  chunk_output_type: console
---

[![status](https://img.shields.io/badge/status-mature%20draft-yellow)](https://canmod.github.io/macpan2/articles/vignette-status#mature-draft)

```{r settings, echo = FALSE, message=FALSE, warning=FALSE, error=FALSE}
library(macpan2)
library(dplyr)
library(ggplot2)
library(splines)
theme_bw = function() ggplot2::theme_bw(base_size = 18)
```

## Overview

Matrix operations are central to `macpan2` models. The engine supports four ways to multiply two matrices together:

- Standard matrix multiplication (`%*%`)
- Kronecker products (`%x%`)
- Elementwise multiplication with recycling (`*`)
- Left sparse matrix multiplication (`sparse_mat_mult()`)

This vignette introduces each operation and illustrates typical use cases. It depends on the following packages.

```{r pkg}
library(macpan2)
library(dplyr)
library(ggplot2)
library(splines)
```

## Standard Matrix Multiplication (`%*%`)

Dense matrix multiplication works in `macpan2` exactly as in base R.

```{r}
A <- matrix(1:6, 3, 2)
B <- matrix(1:4, 2, 2)
A %*% B
macpan2::engine_eval(~A %*% B, A = A, B = B)
```

## Kronecker Product (`%x%`)

The Kronecker product builds structured block matrices, exactly as in base R.

```{r}
A <- matrix(c(1, 2), nrow = 1)
B <- matrix(c(0, 1, 1, 0), nrow = 2)
A %x% B
macpan2::engine_eval(~A %x% B, A = A, B = B)
```

Kronecker products are useful for replicating structure across groups or compartments, as described in [this article](https://canmod.github.io/macpan2/articles/kronecker.html).


## Elementwise Multiplication with Recycling (`*`)

Elementwise multiplication applies entrywise and supports matrix recycling.

Given $z = x * y$, the entries of $z$ are defined by:

$$
z_{i,j} = \begin{cases}
  x_{i,j} \times y_{i,j} & \text{if } \dim(x) = \dim(y) \\
  x_{i,j} \times y_{i,1} & \text{if } n_{\text{row}}(x) = n_{\text{row}}(y),\ n_{\text{col}}(y) = 1 \\
  x_{i,j} \times y_{1,j} & \text{if } n_{\text{col}}(x) = n_{\text{col}}(y),\ n_{\text{row}}(y) = 1 \\
  x_{i,1} \times y_{i,j} & \text{if } n_{\text{row}}(x) = n_{\text{row}}(y),\ n_{\text{col}}(x) = 1 \\
  x_{1,j} \times y_{i,j} & \text{if } n_{\text{col}}(x) = n_{\text{col}}(y),\ n_{\text{row}}(x) = 1 \\
  x_{1,1} \times y_{i,j} & \text{if } x \text{ is } 1 \times 1 \\
  x_{i,j} \times y_{1,1} & \text{if } y \text{ is } 1 \times 1 \\
\end{cases}
$$

These recycling rules differ from those in base `R`. See [this article](https://canmod.github.io/macpan2/articles/elementwise_binary_operators) for a general overview and examples of how `macpan2` handles elementwise binary operators.

### Example of Age-Structured Transmission with Elementwise Matrix Multiplication

We illustrate how a structured force of infection can be built inside a `macpan2` model using elementwise matrix multiplication. 

We define the matrices used to construct the force of infection: a 2×1 susceptibility matrix, a 2×2 contact matrix with rows summing to 1, and a 1×2 infectivity matrix.

```{r force-of-infection-components}
susceptibility <- matrix(c(0.8, 1.2), ncol = 1,
  dimnames = list(sus = c("young", "old"), NULL)
)

contact <- matrix(c(0.6, 0.4,
                    0.3, 0.7), nrow = 2, byrow = TRUE,
  dimnames = list(sus = c("young", "old"),
                  inf = c("young", "old"))
)

infectivity <- matrix(c(1.0, 0.5), nrow = 1,
  dimnames = list(NULL, inf = c("young", "old"))
)

print(susceptibility)
print(contact)
print(infectivity)
```

We next define the model specification. The `before` block uses elementwise matrix multiplication to construct a transmission matrix `B` from `susceptibility`, `contact`, and `infectivity`. This is multiplied by normalized prevalence to produce the force of infection `lambda`.

```{r model-definition}
spec <- mp_tmb_model_spec(
  before = list(
        N ~ S + I
      , B ~ beta * susceptibility * contact * infectivity
      , lambda ~ B %*% (I / N)
  ),
  during = list(
    mp_per_capita_flow(
      from = "S", to = "I", rate = "lambda",
      flow_name = "infection"
    )
  ),
  default = nlist(
        beta = 0.8
      , S = c(young = 90, old = 90)
      , I = c(young = 10, old = 10)
      , N = c(young = 100, old = 100)
      , susceptibility, contact, infectivity
      , infection = c(young = NA_real_, old = NA_real_)
  )
)
```

Note the specification of the `infection` vector in the `default` list so that the names `young` and `old` can be provided in the simulated outputs.

We next run the simulation for 50 time steps and plot the trajectories of susceptibles, infecteds, and new infections, stratified by age group.

```{r simulate-and-plot, fig.width=5, fig.height=6}
library(dplyr)
library(ggplot2)

result <- (spec
  |> mp_simulator(
       time_steps = 50,
       outputs = c("S", "I", "infection")
     )
  |> mp_trajectory()
  |> mutate(
       variable = factor(matrix, levels = c("S", "I", "infection")),
       age = factor(row, levels = c("young", "old"))
     )
)

(result
  |> ggplot()
  + aes(time, value)
  + geom_line()
  + facet_grid(variable ~ age, scales = "free")
  + theme_bw()
)
```

Note that elementwise matrix multiplication with row and column vectors is equivalent to standard matrix multiplication with diagonal matrices. That is, in `macpan2` `susceptibility * contact * infectivity` behaves like `diag(susceptibility) %*% contact %*% diag(infectivity)`, but with fewer zeros.

## Multiplying a Sparse Matrix with a Dense Matrix

It is often necessary to perform matrix multiplications where the left-hand matrix is sparse—meaning most of its entries are zero. Passing large sparse matrices as dense matrices with zeros into `macpan2` models can lead to unnecessary memory use and performance slowdowns.

To address this, `macpan2` provides the engine function `sparse_mat_mult()`, which multiplies a sparse matrix (given in compressed form) by a dense matrix. Instead of passing the full sparse matrix, you pass:

- `x` — non-zero values
- `i` — zero-based row indices
- `j` — zero-based column indices
- `y` — dense right-hand matrix
- `z` — pre-allocated output matrix

### Extracting Sparse Representation

To simplify the creation of sparse representations, `macpan2` provides the helper function `sparse_matrix_notation()`. This function takes a dense matrix and returns:

```{r}
A <- matrix(c(5, 0, 0,
              0, 0, 3,
              0, 2, 0), nrow = 3, byrow = TRUE)

A_sparse <- sparse_matrix_notation(A, zero_based = TRUE)
print(A_sparse)

total_entries <- length(A)
non_zero_entries <- length(A_sparse$values)
sparsity <- 100 * (1 - non_zero_entries / total_entries)
cat(sprintf("Matrix sparsity: %.1f%%\n", sparsity))
```

### Small Example of Sparse Matrix Multiplication

We illustrate sparse matrix multiplication by continuing the example where matrix `A` is represented by the sparse object `A_sparse`, and multiplying `A` by a dense matrix `y`.
```{r}
y <- matrix(c(1, 4,
              2, 5,
              3, 6), nrow = 3, byrow = TRUE)

# Pre-allocate output matrix (3×2)
z <- matrix(0, nrow = 3, ncol = 2)

# Model specification with sparse_mat_mult
spec <- mp_tmb_model_spec(
  before = z ~ sparse_mat_mult(x, i, j, y, z),
  default = nlist(x = A_sparse$values, y, z),
  integers = list(i = A_sparse$row_index, j = A_sparse$col_index)
)

# Run simulation for zero time steps to
# execute 'before' block only
result <- (spec
  |> mp_simulator(time_steps = 0, outputs = "z")
  |> mp_final_list()
)

# Result from macpan2
print(result$z)

# Direct multiplication for validation
A %*% y
```

- `sparse_mat_mult()` multiplies a sparse matrix (given as values and indices) by a dense matrix without constructing the full sparse matrix.
- The helper `sparse_matrix_notation()` extracts the required sparse representation from any matrix, applying zero-based indexing and a numerical tolerance for treating near-zero elements as exactly zero.
- The function writes the result into `z`, which must be allocated beforehand.

### Example of Sparse Matrix Multiplication to Model Time-Varying Transmission Rate
 
We implement an SI model where the transmission rate $\beta(t)$ varies over time, modelled as a spline basis expansion:
$$
\log \beta(t) = B(t) \cdot \theta
$$
We use `sparse_matrix_notation()` to encode the spline basis sparsely and compute $\log \beta(t)$ using `sparse_mat_mult()` in the `before` block. In this case, the spline basis contains no exact zeros, but many near-zero values that contribute little to the result. The tolerance ensures these negligible entries are dropped, improving sparsity without meaningfully affecting the result. For basis matrices like these, choosing a reasonable tolerance (e.g., `1e-4`) helps strike a balance between computational efficiency and model fidelity.

We compute $\log \beta(t)$ from a sparse spline basis.

```{r, fig.height = 8, fig.width = 4}
# Time points for simulation (100 steps)
n_steps <- 100
time_points <- seq(0, 1, length.out = n_steps)

# Create a cubic B-spline basis with 8 degrees of freedom
B_dense <- splines::bs(time_points, df = 8, degree = 3, intercept = TRUE)

# Convert basis to sparse form with small tolerance
B_sparse <- sparse_matrix_notation(B_dense
  , zero_based = TRUE
  , tol = 1e-4
)

# Print basis sparsity
total_entries <- length(B_dense)
non_zero_entries <- length(B_sparse$values)
sparsity <- 100 * (1 - non_zero_entries / total_entries)
cat(sprintf("Spline basis matrix sparsity (tol = 1e-4): %.1f%%\n", sparsity))

# Example spline coefficients for log(beta)
set.seed(12)
theta <- rnorm(8, -2, 1)

# Pre-allocate beta_log[t] for 100 time steps
beta_log <- matrix(0, nrow = n_steps, ncol = 1)

# Define SI model with time-varying beta
spec <- mp_tmb_model_spec(
  before = beta_log ~ sparse_mat_mult(x, i, j, theta, beta_log),
  during = list(
    beta ~ exp(beta_log[time_step(1)]),
    mp_per_capita_flow(
      from = "S",
      to = "I",
      rate = "beta * I / N",
      flow_name = "infection"
    )
  ),
  default = nlist(N = 100, S = 99, I = 1, x = B_sparse$values, theta, beta_log),
  integers = nlist(i = B_sparse$row_index, j = B_sparse$col_index)
)

# Simulate for 100 time steps
result <- (spec
  |> mp_simulator(
      time_steps = n_steps
    , outputs = c("S", "I", "beta", "infection")
  )
  |> mp_trajectory()
  |> mutate(variable = factor(
      matrix
    , levels = c("S", "I", "beta", "infection")
  ))
)

# Plot the results
ggplot(result, aes(time, value)) +
  geom_line() +
  facet_wrap(~variable, scales = "free_y", ncol = 1) +
  theme_bw()
```

- Transmission rate $\beta(t)$ is computed dynamically using `sparse_mat_mult()`.
- The spline basis is passed in sparse form, allowing efficient calculation of $\beta(t)$ at each time step.
- The SI model uses `mp_per_capita_flow()` for infection, with time-varying transmission.
- For this example, the spline basis (with tolerance $10^{-4}$) has approximately `r sprintf("%.1f%% sparsity", sparsity)`.
- Larger models with higher sparsity will benefit more from this approach.

## Summary of Matrix Operations

| Operation           | Description                               |
| ------------------- | ----------------------------------------- |
| `%*%`               | Dense matrix multiplication               |
| `%x%`               | Kronecker product                         |
| `*`                 | Elementwise multiplication with recycling |
| `sparse_mat_mult()` | Sparse × Dense multiplication             |

## When to Use Each

- Dense × Dense (`%*%`) : Use for small/moderate dense matrices.
- Kronecker (`%x%`) : Use for structured expansions ([example](https://canmod.github.io/macpan2/articles/kronecker.html)).
- Elementwise (`*`) : Use for scaling rates in structured models ([example](#example-of-age-structured-transmission-with-elementwise-matrix-multiplication)).
- Sparse × Dense (`sparse_mat_mult()`) : Use for large, sparse left-hand matrices ([example](#example-of-sparse-matrix-multiplication-to-model-time-varying-transmission-rate)).
