## ----setup, include=FALSE-----------------------------------------------------
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(vbpm)

## ----population---------------------------------------------------------------
K <- 3
ipf <- 6
J <- K * ipf
cluster <- rep(seq_len(K), each = ipf)

set.seed(7)
mla <- matrix(0, J, K + 1)
mla[, 1] <- round(runif(J, .35, .75), 2)
for (k in seq_len(K)) {
  mla[cluster == k, k + 1] <- round(runif(ipf, .35, .70), 2)
}
while (max(rowSums(mla^2)) > .9) mla <- round(mla * .97, 3)

sim <- sim_fa(N = 800, mla = mla, phi = 0, rseed = 1)
Y <- sim$dat

## ----backbones----------------------------------------------------------------
Q0_AO <- matrix(-1L, J, 2)
for (k in 1:2) Q0_AO[which(cluster == k)[1:2], k] <- 1L

Q0_AZ <- Q0_AO
for (k in 1:2) {
  anchors <- which(cluster == k)[1:2]
  Q0_AZ[anchors, ] <- 0L
  Q0_AZ[anchors, k] <- 1L
}

Q0_show <- cbind(Q0_AO[1:8, ], Q0_AZ[1:8, ])
colnames(Q0_show) <- c("AO_1", "AO_2", "AZ_1", "AZ_2")
Q0_show

## ----step1--------------------------------------------------------------------
AO <- pefa(Q0_AO, Y, Kmin = 2, Kmax = 5, verbose = FALSE)
AZ <- pefa(Q0_AZ, Y, Kmin = 2, Kmax = 5, verbose = FALSE)

AO$sweep[, c("K", "ELBO", "AIC", "BIC", "SRMR", "CFI")]
AZ$sweep[, c("K", "ELBO", "AIC", "BIC", "SRMR", "CFI")]

## ----step1-transitions--------------------------------------------------------
AO$transitions[, c("K_from", "K_to", "ELBO_gain_pct", "BIC_gain_pct",
                   "phi_min", "rmsd_max", "unmatched_ssl", "collision")]
AZ$transitions[, c("K_from", "K_to", "ELBO_gain_pct", "BIC_gain_pct",
                   "phi_min", "rmsd_max", "unmatched_ssl", "collision")]

## ----step1-persistence--------------------------------------------------------
round(AO$persistence$phi, 3)
round(AZ$persistence$phi, 3)
ssl(AO)[c("3", "4")]
ssl(AZ)[c("3", "4")]

## ----step1-reading------------------------------------------------------------
## Analysis-owned profiles. ELBO and BIC each use the full fitted window,
## a 20% gain cutoff, sustain one, a post-peak strict crossing, and no fallback.
profile_specs <- data.frame(
  profile = c(".85/r1", ".80/r2", ".70/r3"),
  phi_cut = c(.85, .80, .70), r = 1:3,
  practical_default = c(FALSE, TRUE, FALSE)
)
profile_sources <- 2:3

read_sweep <- function(x, phi_cut, r, sources) {
  K <- as.integer(x$sweep$K)
  converged <- x$sweep$converged

  read_count <- function(score) {
    usable <- length(K) >= 2L && all(is.finite(K)) && all(diff(K) == 1L) &&
      length(score) == length(K) && all(is.finite(score)) &&
      is.logical(converged) && all(converged %in% TRUE)
    if (!usable) return(list(usable = FALSE, Khat = NA_integer_))
    gain <- diff(score)
    gain_max <- max(gain)
    if (!(gain_max > 0)) return(list(usable = TRUE, Khat = NA_integer_))
    peak <- max(which(gain == gain_max))
    hit <- which(seq_along(gain) > peak & gain < .20 * gain_max)
    list(usable = TRUE,
         Khat = if (length(hit)) K[hit[1L]] else NA_integer_)
  }

  ELBO <- read_count(x$sweep$ELBO)
  BIC <- read_count(-x$sweep$BIC)
  C20 <- sort(unique(c(ELBO$Khat, BIC$Khat)[
    is.finite(c(ELBO$Khat, BIC$Khat))]))

  phi <- x$persistence$phi
  collision <- x$persistence$collision
  conv <- setNames(converged, K)
  state <- vapply(sources, function(k) {
    targets <- k + seq_len(r)
    edge <- vapply(targets, function(target) {
      from <- as.character(k); to <- as.character(target)
      if (!from %in% rownames(phi) || !to %in% colnames(phi) ||
          !isTRUE(conv[[from]]) || !isTRUE(conv[[to]]) ||
          is.na(collision[from, to])) return(NA_integer_)
      if (isTRUE(collision[from, to])) return(0L)
      value <- phi[from, to]
      if (!is.finite(value)) NA_integer_ else as.integer(value >= phi_cut)
    }, integer(1))
    if (any(edge %in% 0L)) 0L else
      if (length(edge) && all(edge %in% 1L)) 1L else NA_integer_
  }, integer(1))

  persistent <- which(state == 1L)
  if (!length(state)) {
    resolution <- "unresolved"
    Kp <- NA_integer_
  } else if (!length(persistent)) {
    resolution <- if (all(state %in% 0L)) "nonpersistent" else "unresolved"
    Kp <- NA_integer_
  } else {
    top <- max(persistent)
    higher <- if (top < length(state)) state[(top + 1L):length(state)] else 0L
    resolution <- if (all(higher %in% 0L)) "persistent" else "unresolved"
    Kp <- if (resolution == "persistent") sources[top] else NA_integer_
  }

  hit_ELBO <- is.finite(Kp) && is.finite(ELBO$Khat) && ELBO$Khat == Kp
  hit_BIC <- is.finite(Kp) && is.finite(BIC$Khat) && BIC$Khat == Kp
  support <- if (hit_ELBO && hit_BIC) "both" else if (hit_ELBO) "ELBO only" else
    if (hit_BIC) "BIC only" else "none"
  any_count_usable <- ELBO$usable || BIC$usable
  layer <- if (resolution == "persistent" && Kp %in% C20) "L1" else
    if (resolution == "persistent" && any_count_usable) "L2" else
    if (resolution == "nonpersistent") "L3" else
      "unclassified"
  gap <- if (resolution == "persistent" && length(C20))
    min(abs(C20 - Kp)) else NA_real_

  list(Khat_ELBO = ELBO$Khat, Khat_BIC = BIC$Khat, C20 = C20,
       count_usable = c(ELBO = ELBO$usable, BIC = BIC$usable),
       support = support, Kp = Kp, source_state = setNames(state, sources),
       resolution = resolution, layer = layer, g_CP = gap)
}

profile_row <- function(x, backbone, spec) {
  z <- read_sweep(x, spec$phi_cut, spec$r, profile_sources)
  data.frame(backbone = backbone, profile = spec$profile,
             practical_default = spec$practical_default,
             Khat_ELBO = z$Khat_ELBO, Khat_BIC = z$Khat_BIC,
             C20 = if (length(z$C20)) paste0("{", paste(z$C20,
               collapse = ","), "}") else "{}",
             support = z$support, Kp = z$Kp, layer = z$layer,
             g_CP = z$g_CP, row.names = NULL)
}

sweeps <- list(AO = AO, AZ = AZ)
profiles <- do.call(rbind, lapply(names(sweeps), function(backbone)
  do.call(rbind, lapply(seq_len(nrow(profile_specs)), function(i)
    profile_row(sweeps[[backbone]], backbone, profile_specs[i, ])))))
profiles

## ----step2-design-------------------------------------------------------------
default_AO <- profiles[profiles$backbone == "AO" &
                       profiles$profile == ".80/r2", , drop = FALSE]
if (nrow(default_AO) != 1L || !default_AO$layer %in% c("L1", "L2"))
  stop("The declared profile did not deliver a Step-2 proposal.")
K_step2 <- as.integer(default_AO$Kp)
K0      <- ncol(AO$Q0)
Lam     <- AO$loadings[[as.character(K_step2)]]
d     <- unique(colSums(AO$Q0 == 1L))
if (length(d) != 1L || d[1] < 1L) {
  stop("Choose one marker depth before building Q2.")
}

Q2 <- cbind(AO$Q0, matrix(-1L, J, K_step2 - K0))
colnames(Q2) <- paste0("F", seq_len(K_step2))
if (K_step2 > K0) {
  for (k in (K0 + 1L):K_step2) {
    top <- head(order(-abs(Lam[, k]), seq_len(J)), d)
    Q2[top, k] <- 1L
  }
}

c(K_step2 = K_step2, K0 = K0, depth = d)
Q2[rowSums(Q2 == 1L) > 0L, , drop = FALSE]

## ----step2-fit----------------------------------------------------------------
f_obl <- vbfa(Y, Q2)
f_bif <- vbfa(Y, Q2, bifactor = TRUE)

idx <- c("RMSEA", "SRMR", "CFI", "TLI", "AIC", "BIC")
round(rbind(
  oblique  = fit_stats(f_obl)[idx],
  bifactor = fit_stats(f_bif)[idx]
), 3)

## ----step2-effects------------------------------------------------------------
special_effects(f_bif)

## ----bifactor-sweep-----------------------------------------------------------
BI <- pefa(Q0_AO, Y, Kmin = 2, Kmax = 5, bifactor = TRUE, verbose = FALSE)
BI$sweep[, c("K", "ELBO", "AIC", "BIC", "SRMR", "CFI")]
BI$transitions[, c("K_from", "K_to", "ELBO_gain_pct", "BIC_gain_pct",
                   "phi_min", "rmsd_max", "unmatched_ssl", "collision")]
round(BI$persistence$phi, 3)
round(BI$persistence$rmsd, 3)
ssl(BI)

