Skip to contents

extract_coevolution_modules() standardizes a component-specific cross-lineage covariance block and decomposes it into coupled trait axes. For a named coevolution kernel tier, the helper computes R = Sigma_row^{-1/2} Gamma Sigma_col^{-1/2} from the shared covariance returned by extract_Sigma(), then applies a singular-value decomposition.

This is a point-estimate derived-output helper, covered by the package's validation tests, for fitted dense-kernel coevolution models whose component-specific Gamma blocks are already extractable with extract_Gamma(). It does not report uncertainty, choose the biological rank, estimate rho, or calibrate null thresholds. Use simulation, bootstrap, and kernel-separability checks before treating the returned axes as scientific evidence.

Usage

extract_coevolution_modules(
  fit,
  level,
  row_traits,
  col_traits,
  scale = c("shape", "effect"),
  n_modules = NULL,
  tol = sqrt(.Machine$double.eps)
)

Arguments

fit

A fitted gllvmTMB_multi object.

level

Character scalar naming the fitted covariance tier.

row_traits, col_traits

Character vectors naming the row-lineage and column-lineage traits.

scale

Character scalar. "shape" (default) uses Gamma_shape = Lambda_row Lambda_col^T; "effect" uses the fixed-rho Gamma_effect available for kernels built by make_cross_kernel().

n_modules

Optional positive integer limiting the number of returned singular axes. By default all axes are returned.

tol

Numerical tolerance for the generalized inverse square roots of the within-lineage shared covariance blocks.

Value

A list with R, modules, row_axes, and col_axes. R is the standardized cross-lineage correlation-like block. modules has one row per singular axis with its singular value and squared-value share. row_axes and col_axes contain trait loadings for each coupled axis.

Examples

set.seed(1)
## A small host-partner system: 12 host and 12 partner species. Each
## lineage has its own similarity matrix. Here similarity decays with
## distance along a species ordering, a stand-in for a phylogenetic
## correlation matrix such as ape::vcv(tree, corr = TRUE).
host <- paste0("H", 1:12)
partner <- paste0("P", 1:12)
A_H <- 0.6^abs(outer(1:12, 1:12, "-"))
A_P <- A_H
dimnames(A_H) <- list(host, host)
dimnames(A_P) <- list(partner, partner)
## W records which hosts interact with which partners: host i interacts
## with partners i - 1, i and i + 1.
W <- 1 * (abs(outer(1:12, 1:12, "-")) <= 1)
dimnames(W) <- list(host, partner)
## One kernel over all 24 species; the fixed rho sets how strongly
## interacting hosts and partners are linked.
K <- make_cross_kernel(A_H, A_P, W, rho = 0.9)

## Simulate one latent axis shared by all four traits. Host traits are
## measured on hosts only and partner traits on partners only: four
## replicate measurements per species, each with noise (SD 0.3).
lambda <- c(h_size = 0.9, h_defence = 0.6, p_size = 0.8, p_attack = -0.5)
z <- drop(t(chol(K)) %*% rnorm(24))
names(z) <- rownames(K)
dat <- expand.grid(rep = 1:4, trait = names(lambda),
                   species = rownames(K), stringsAsFactors = FALSE)
dat <- dat[startsWith(dat$species, "H") == startsWith(dat$trait, "h_"), ]
dat$value <- lambda[dat$trait] * z[dat$species] +
  rnorm(nrow(dat), sd = 0.3)
dat$trait <- factor(dat$trait, levels = names(lambda))
dat$species <- factor(dat$species, levels = rownames(K))
## Each record (one species, one replicate) is a sampling unit
dat$obs <- factor(paste(dat$species, dat$rep))

## One latent axis whose species scores are correlated as in K. The
## term is named "cross"; the functions below refer to it by name.
fit <- gllvmTMB(
  value ~ 0 + trait + kernel_latent(species, K = K, d = 1, name = "cross"),
  data = dat, trait = "trait", unit = "obs", cluster = "species"
)
## Coupled host and partner trait axes (point estimates only). With
## one latent axis (d = 1) there is a single module (n_modules = 1),
## and its singular value is 1 by construction, so read the trait
## loadings: hosts with larger size and defence are linked with
## partners of larger size and lower attack. Flipping the signs of
## both axes gives the same module.
mods <- extract_coevolution_modules(
  fit,
  level = "cross",
  row_traits = c("h_size", "h_defence"),
  col_traits = c("p_size", "p_attack"),
  n_modules = 1
)
mods$modules
#>   component   module singular_value squared_share
#> 1     cross module_1              1             1
mods$row_axes
#>   component side   module     trait    loading
#> 1     cross  row module_1    h_size -0.8315609
#> 2     cross  row module_1 h_defence -0.5554336
mods$col_axes
#>   component   side   module    trait    loading
#> 1     cross column module_1   p_size -0.8799045
#> 2     cross column module_1 p_attack  0.4751505