# Network-Based Item Selection

`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:

    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:

    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:

    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)
    #> [1] 26

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

    # 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.