---
title: "Network-Based Item Selection"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Network-Based Item Selection}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(meow)
```

`select_max_dist()` selects items using the *entire* item-exposure history. It
treats the item pool as a weighted graph and administers the item farthest, in
shortest-path distance, from the items a respondent has already seen. This
balances exposure control against measurement efficiency.

# Mathematical foundation

The item pool is a weighted graph: nodes are items, and edge weights derive from
the co-exposure matrix `adj_mat` (entry $(i, j)$ is the number of respondents who
have seen both items). The Floyd--Warshall algorithm (`Rfast::floyd()`) turns the
edge-weight matrix $W$ into an all-pairs shortest-path distance matrix $D$. For
each respondent, the distance of a candidate item to the set of administered items
is the minimum distance to any of them, and the farthest candidate is
administered (ties broken by maximum information).

# Edge weight strategies

The edge-weight function maps co-exposure counts to graph weights, and the choice
shapes behavior. All of these are bundled and unchanged:

```{r, eval = FALSE}
edge_weight_inverse(adj_mat, alpha = 1)              # 1 / (adj_mat + alpha)
edge_weight_negative_log(adj_mat, alpha = 1)         # -log(adj_mat + alpha)
edge_weight_linear(adj_mat, max_co_responses = NULL) # adj_mat / max(adj_mat)
edge_weight_power(adj_mat, beta = 0.5, alpha = 1)    # (adj_mat + alpha)^beta
edge_weight_exponential(adj_mat, lambda = 0.1)       # exp(-lambda*(adj_mat+alpha))
```

* **Inverse / negative log / exponential**: more co-responses give *smaller*
  weights, so frequently co-administered items are "closer" and the algorithm
  spreads exposure across dissimilar items.
* **Linear**: more co-responses give *larger* weights, inverting that logic.
* **Power**: `beta < 1` dampens and `beta > 1` amplifies the effect of high
  co-response counts.

# Implementation

`select_max_dist()` follows the item selection contract
(`vignette("item-selection")`): it works on the matrix administration state and
returns an updated `admin`. After the distance matrix is computed, the per-item
distances are obtained with `Rfast::colMins()` rather than a row-wise data-frame
operation:

```{r, eval = FALSE}
select_max_dist <- function(pers, item, R, admin, adj_mat = NULL, n_candidates = 1) {
  if (!any(admin != 0)) {
    admin[, seq_len(min(5, ncol(admin)))] <- 1L     # seed five items
    return(admin)
  }
  dist_mat <- Rfast::floyd(1 / adj_mat)              # all-pairs shortest paths
  info <- {                                          # 2PL information matrix
    lin <- sweep(outer(pers$theta, item$b, "-"), 2, item$a, "*")
    P <- stats::plogis(lin); sweep(P * (1 - P), 2, item$a^2, "*")
  }
  for (i in which(rowSums(admin == 0) > 0)) {
    administered <- which(admin[i, ] != 0)
    candidates   <- which(admin[i, ] == 0)
    sub <- dist_mat[administered, candidates, drop = FALSE]
    cand_dist <- if (length(administered) == 1L) sub[1, ] else Rfast::colMins(sub, value = TRUE)
    pool <- candidates[cand_dist >= max(cand_dist)]  # farthest items
    admin[i, pool[which.max(info[i, pool])]] <- 1L   # tie-break by information
  }
  admin
}
```

`select_max_dist_enhanced()` is identical except that the edge weights come from a
user-supplied `edge_weight_fun` applied to `adj_mat` before `Rfast::floyd()`.

# Using different edge weight strategies

A small runnable example with the default inverse weights:

```{r}
sim <- meow(
  select_fun  = select_max_dist,
  update_fun  = update_theta_mle,
  data_loader = data_simple_1pl,
  data_args   = list(N_persons = 50, N_items = 30),
  select_args = list(n_candidates = 3),
  fix         = "item"
)
nrow(sim$results)
```

Swap in a different edge-weight function through `select_max_dist_enhanced()`:

```{r, eval = FALSE}
# Power transformation with beta = 0.3
meow(
  select_fun  = select_max_dist_enhanced,
  update_fun  = update_theta_mle,
  data_loader = data_simple_1pl,
  data_args   = list(N_persons = 100, N_items = 50),
  select_args = list(
    n_candidates = 3,
    edge_weight_fun = edge_weight_power,
    edge_weight_args = list(beta = 0.3, alpha = 1)
  ),
  fix = "item"
)
```

# Choosing a strategy

| Strategy | Goal | Trade-off |
|----------|------|-----------|
| Inverse (default) | spread exposure across dissimilar items | may over-expose clusters |
| Linear | keep item clusters / topic areas together | can reduce efficiency |
| Power | tune sensitivity to co-response counts | requires choosing `beta` |
| Exponential | strong exposure control | can reduce efficiency |

# Considerations

* `Rfast::floyd()` is $O(n^3)$ in the number of items and is run each iteration,
  so network selection is more expensive than `select_max_info()`.
* Experiment with `n_candidates` (1--5) to trade exposure control against
  measurement efficiency, and compare against simpler selectors as a baseline.
