--- title: "Simulated Annealing with simulatedAnnealing()" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Simulated Annealing with simulatedAnnealing()} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(ShortForm) ``` ## How it works Simulated annealing mimics the physical process of annealing metals. [Kirkpatrick et al. (1983)](https://doi.org/10.1126/science.220.4598.671) introduced the analogy; `simulatedAnnealing()` follows that demonstration closely, adapted for psychometric models. At each step: 1. A "neighbor" candidate model is generated from the current model (by default, swapping one or more items for a short form, or freeing/fixing a parameter for a full-model search). 2. The neighbor's `criterion` value is compared to the current model's. 3. If the neighbor is better, it's accepted. If it's worse, it's accepted anyway with some probability that depends on how much worse it is and the current "temperature" -- which shrinks toward zero as the search progresses, so the algorithm accepts worse moves more readily early on (to escape local optima) and becomes greedier as it converges. 4. The best model found so far is tracked separately from the current model, since the current model can still wander to worse solutions. ## A basic example As with `antColony()`, every candidate item must already appear on its factor's line in `initialModel` -- factors and each factor's candidate item pool are derived directly from that syntax. ```{r basic-example} set.seed(58310) result <- suppressWarnings(simulatedAnnealing( initialModel = " visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9 ", originalData = lavaan::HolzingerSwineford1939, maxIterations = 3, criterion = "cfi", negateCriterion = TRUE, itemsPerFactor = c(2, 2, 2), items = paste0("x", 1:9) )) result ``` `itemsPerFactor` sets the target number of items to keep per factor (in the order factors appear in `initialModel`); `items` is the flat pool of candidate item names to draw from (defaulting to all column names in `originalData` if omitted). ## Inspecting the result ```{r summary} summary(result) ``` `plot()` shows the criterion value across chain steps. Since the algorithm can wander to worse solutions before recovering, this trace is not necessarily monotonic -- the *best* model found is what's returned in `result@best_model`, not necessarily the last one visited. ```{r plot, fig.width=6, fig.height=4} plot(result) ``` An early "burn-in" period (common practice for Monte Carlo-style methods) can be excluded from the plot: ```{r plot-burnin, fig.width=6, fig.height=4} plot(result, burn_in = 1) ``` ## The criterion `criterion` accepts either a `character` fit-measure name recognized by `lavaan::fitmeasures()` (as above), or an arbitrary function that takes a fitted `lavaan` object and returns a single numeric value: ```{r criterion-function, eval=FALSE} simulatedAnnealing( initialModel = "...", originalData = myData, maxIterations = 20, criterion = function(fit) AIC(fit), negateCriterion = FALSE, # smaller AIC is better itemsPerFactor = c(6, 6, 6) ) ``` `negateCriterion` controls the search direction: `TRUE` looks for the *largest* value of `criterion` (e.g. CFI, where larger is better), `FALSE` looks for the *smallest* (e.g. RMSEA or AIC, where smaller is better). `criterion`/`negateCriterion` is supplied on its natural scale either way -- you never need to pre-negate your own criterion function. ## Full-model (non-short-form) searches Omitting `itemsPerFactor` switches from item-swap short-form search to a full-model search, where each step frees or fixes one parameter rather than swapping items: ```{r full-model-example, eval=FALSE} fittedModel <- lavaan::cfa( model = " visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9", data = lavaan::HolzingerSwineford1939 ) simulatedAnnealing( initialModel = fittedModel, originalData = lavaan::HolzingerSwineford1939, maxIterations = 20, criterion = "cfi", negateCriterion = TRUE ) ``` ## Bifactor models Pass the name of the general factor as `bifactor` to have all of the retained items across the other factors also load on it -- as with `antColony()`, this only applies when creating a short form (`itemsPerFactor` supplied): ```{r bifactor-example, eval=FALSE} bifactorModel <- " visual =~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9" simulatedAnnealing( initialModel = bifactorModel, originalData = lavaan::HolzingerSwineford1939, maxIterations = 20, criterion = "cfi", negateCriterion = TRUE, itemsPerFactor = c(6, 3, 3), items = paste0("x", 1:9), bifactor = "visual" ) ``` ## Parallel chains `setChains` runs multiple independent searches in parallel (each starting from the same `initialModel` but exploring different random neighbors), which is a useful check against any one chain getting stuck: ```{r parallel-chains, eval=FALSE} simulatedAnnealing( initialModel = "...", originalData = myData, maxIterations = 100, criterion = "cfi", negateCriterion = TRUE, itemsPerFactor = c(6, 6, 6), setChains = 4 ) ``` With `setChains > 1`, `plot()` overlays each chain's trace (up to 8 chains), and the best model/fit reported is the best across all chains.