---
title: "Changing residual coupling with rho12"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Changing residual coupling with rho12}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
if (!"package:drmTMB" %in% search()) {
  library(drmTMB)
}
```

This tutorial introduces bivariate location-scale regression with
predictor-dependent residual correlation. It assumes the univariate
location-scale reading pattern from [When variance carries
signal](location-scale.html) and extends it to two responses. The scientific
idea is that two responses can change in mean, change in residual variance,
and change in residual coupling.

Current fixed-effect syntax uses one formula per distributional parameter:

```r
drmTMB(
  drm_formula(
    mu1 = y1 ~ x1 + x2,
    mu2 = y2 ~ x1,
    sigma1 = ~ x1 + x2,
    sigma2 = ~ x1,
    rho12 = ~ x1 + x2
  ),
  family = c(gaussian(), gaussian()),
  data = dat
)
```

The central modelling question is whether predictors change residual coupling
between two responses, not only their means or variances.

The key parameter is `rho12`. A location-scale model asks whether predictors
change residual SDs. A location-coscale model asks whether predictors also
change the residual covariance structure between two responses; in the current
bivariate Gaussian model, that structure is the residual correlation `rho12`.
In ecology and evolution, this can ask whether the residual association between
traits such as body mass and litter size differs among terrestrial, aquatic,
and aerial lifestyles after modelling response-specific means and dispersion.

## Equations for the implemented model

For observation `i`, the implemented fixed-effect bivariate Gaussian model is:

\[
\begin{bmatrix}
y_{1i} \\
y_{2i}
\end{bmatrix}
\mid
\mu_{1i}, \mu_{2i}, \sigma_{1i}, \sigma_{2i}, \rho_{12i}
\sim
\operatorname{MVN}
\left(
\begin{bmatrix}
\mu_{1i} \\
\mu_{2i}
\end{bmatrix},
\Omega_i
\right).
\]

The five distributional predictors are:

\[
\begin{aligned}
\mu_{1i} &= X_{\mu 1,i}\beta_{\mu 1}, \\
\mu_{2i} &= X_{\mu 2,i}\beta_{\mu 2}, \\
\log(\sigma_{1i}) &= X_{\sigma 1,i}\beta_{\sigma 1}, \\
\log(\sigma_{2i}) &= X_{\sigma 2,i}\beta_{\sigma 2}, \\
\eta_{\rho12,i} &= X_{\rho12,i}\beta_{\rho12}, \\
\rho_{12i} &= \tanh(\eta_{\rho12,i}).
\end{aligned}
\]

The residual covariance matrix is:

\[
\Omega_i =
\begin{bmatrix}
\sigma_{1i}^2 & \rho_{12i}\sigma_{1i}\sigma_{2i} \\
\rho_{12i}\sigma_{1i}\sigma_{2i} & \sigma_{2i}^2
\end{bmatrix}.
\]

Component meanings:

| Component | Meaning |
|---|---|
| `mu1`, `mu2` | expected values for responses 1 and 2 |
| `sigma1`, `sigma2` | residual SDs for responses 1 and 2 |
| `eta_rho12` | unconstrained linear predictor for residual correlation |
| `rho12` | residual response-response correlation within observation `i` |
| `Omega_i` | residual covariance matrix after means and residual SDs are modelled |

For implementation cross-checking, the same model can also be written in a
compact code-like form:

```text
mu1_i = X_mu1[i, ] beta_mu1
mu2_i = X_mu2[i, ] beta_mu2
log(sigma1_i) = X_sigma1[i, ] beta_sigma1
log(sigma2_i) = X_sigma2[i, ] beta_sigma2
eta_rho12_i = X_rho12[i, ] beta_rho12
rho12_i = tanh(eta_rho12_i)
Omega_i[1, 1] = sigma1_i^2
Omega_i[2, 2] = sigma2_i^2
Omega_i[1, 2] = Omega_i[2, 1] = rho12_i * sigma1_i * sigma2_i
```

Implementation note: `drmTMB` internally evaluates residual correlations as
`0.999999 * tanh(eta_rho12_i)`. The tiny multiplier keeps covariance
matrices strictly positive definite near the boundaries. It is not a
biological scaling factor, and it should not change how users interpret the
model.

The matching `drmTMB` syntax is:

```r
drmTMB(
  bf(
    mu1 = y1 ~ x1 + x2,
    mu2 = y2 ~ x1,
    sigma1 = ~ x1 + x2,
    sigma2 = ~ x1,
    rho12 = ~ x1 + x2
  ),
  family = c(gaussian(), gaussian()),
  data = dat
)
```

The mapping is:

| Symbol | Meaning | R syntax source |
|---|---|---|
| `y1_i`, `y2_i` | observed responses | left-hand sides of `mu1` and `mu2` formulas |
| `X_mu1`, `X_mu2` | response-specific mean design matrices | right-hand sides of `mu1` and `mu2` |
| `mu1_i`, `mu2_i` | fitted response-specific means | columns of `fitted(fit)` |
| `X_sigma1`, `X_sigma2` | response-specific residual scale design matrices | right-hand sides of `sigma1` and `sigma2` |
| `X_rho12` | residual correlation design matrix | right-hand side of `rho12` |
| `sigma1_i`, `sigma2_i` | residual SDs for responses 1 and 2 | `predict(fit, dpar = "sigma1")`, `predict(fit, dpar = "sigma2")` |
| `rho12_i` | residual response-response correlation | `rho12(fit)` |

The fixed effects may be the same, overlapping, or different across
parameters. For example, `x1` can appear in all five formulae, while `x2` might
appear only in `mu1`, `sigma1`, and `rho12`. Each parameter still gets its own
coefficient vector.

Read those coefficient vectors as separate biological statements:

| Formula | Example coefficient | Interpretation |
|---|---|---|
| `mu1 = y1 ~ x` | `mu1:x` | predictor effect on the expected value of response 1 |
| `mu2 = y2 ~ x` | `mu2:x` | predictor effect on the expected value of response 2 |
| `sigma1 = ~ x` | `sigma1:x` | multiplicative change in residual SD for response 1 |
| `sigma2 = ~ x` | `sigma2:x` | multiplicative change in residual SD for response 2 |
| `rho12 = ~ x` | `rho12:x` | change in residual response-response coupling after both means and residual SDs are modelled |

The `rho12` slope is not a slope for either response mean. It is a slope on the
correlation-link scale. Use `rho12(fit, newdata = grid)` or a `rho12` curve
when the reader needs the fitted residual correlation on the response scale.

If both responses have the same location predictors, `mvbind()` is a shorthand
for the two location formulas:

```r
drmTMB(
  bf(
    mvbind(y1, y2) ~ x1 + x2,
    sigma1 = ~ x1 + x2,
    sigma2 = ~ x1,
    rho12 = ~ x1 + x2
  ),
  family = c(gaussian(), gaussian()),
  data = dat
)
```

This expands internally to `mu1 = y1 ~ x1 + x2` and
`mu2 = y2 ~ x1 + x2`. Use explicit `mu1` and `mu2` formulas whenever the
responses need different location predictors.

## Worked example: behaviour coupling and disturbance

Suppose an ecologist measures two behaviours, activity and boldness, and wants
to know whether an environmental gradient changes their residual coupling after
accounting for response-specific means and residual SDs. The code below uses
simulated data, but the model structure matches that scientific question.

For this example, the fitted biological model is:

\[
\begin{aligned}
\begin{bmatrix}
\text{activity}_i\\
\text{boldness}_i
\end{bmatrix}
&\sim
\operatorname{MVN}
\left(
\begin{bmatrix}
\mu_{\text{activity},i}\\
\mu_{\text{boldness},i}
\end{bmatrix},
\Omega_i
\right),\\
\mu_{\text{activity},i}
  &= \beta_{a0} + \beta_{a1}\text{food}_i +
     \beta_{a2}\text{temperature}_i,\\
\mu_{\text{boldness},i}
  &= \beta_{b0} + \beta_{b1}\text{food}_i,\\
\log(\sigma_{\text{activity},i})
  &= \gamma_{a0} + \gamma_{a1}\text{food}_i +
     \gamma_{a2}\text{temperature}_i,\\
\log(\sigma_{\text{boldness},i})
  &= \gamma_{b0} + \gamma_{b1}\text{food}_i,\\
\eta_{\rho12,i} &= \delta_0 + \delta_1\text{disturbance}_i,\\
\rho_{12,i} &= \tanh(\eta_{\rho12,i}).
\end{aligned}
\]

Here `rho12` is not the ordinary correlation between raw activity and raw
boldness. It is the residual correlation after the model has already adjusted
for the mean and residual SD predictors in each response.

```{r bivariate-coscale-simulate}
set.seed(1)
n <- 180
dat <- data.frame(
  food = rnorm(n),
  temperature = rnorm(n),
  disturbance = rnorm(n)
)
mu1 <- 0.2 + 0.5 * dat$food + 0.2 * dat$temperature
mu2 <- -0.1 + 0.4 * dat$food
sigma1 <- exp(-0.2 + 0.2 * dat$food - 0.1 * dat$temperature)
sigma2 <- exp(0.1 - 0.2 * dat$food)
eta_rho12 <- -0.1 + 0.4 * dat$disturbance
rho12 <- tanh(eta_rho12)
e1 <- rnorm(n)
e2 <- rho12 * e1 + sqrt(1 - rho12^2) * rnorm(n)
dat$activity <- mu1 + sigma1 * e1
dat$boldness <- mu2 + sigma2 * e2
```

The model lets `food` and `temperature` explain mean activity, lets `food`
explain mean boldness, lets `food` and `temperature` explain residual SD in
activity, lets `food` explain residual SD in boldness, and lets `disturbance`
explain residual correlation:

```{r bivariate-coscale-fit}
fit_biv <- drmTMB(
  drm_formula(
    mu1 = activity ~ food + temperature,
    mu2 = boldness ~ food,
    sigma1 = ~ food + temperature,
    sigma2 = ~ food,
    rho12 = ~ disturbance
  ),
  family = c(gaussian(), gaussian()),
  data = dat
)
```

Check the fit and inspect the coefficient table:

```{r bivariate-coscale-output}
check_drm(fit_biv)
summary(fit_biv)
```

How to read this output:

1. Rows beginning with `mu1:` describe expected activity, and rows beginning
   with `mu2:` describe expected boldness.
2. Rows beginning with `sigma1:` and `sigma2:` describe log residual SDs for
   activity and boldness.
3. Rows beginning with `rho12:` describe the residual activity-boldness
   correlation on the linear-predictor scale. Use `rho12(fit_biv)` or
   `rho12(fit_biv, newdata = ...)` to read that effect as a correlation.

The `rho12` coefficients are on the linear-predictor scale:

```{r bivariate-coscale-rho12}
coef(fit_biv, "rho12")
head(rho12(fit_biv))
```

`corpairs()` gives the same residual correlation in the long correlation-pair
format. The same table shape now also holds fitted ordinary group-level and
structured rows where their covariance routes are fitted, including
phylogenetic, coordinate-spatial, animal-model, and `relmat()` q=2 or constant
q=4 rows. Predictor-dependent spatial, animal, `relmat()`, study-level, q=4,
residual-scale, and slope-specific `corpair()` regressions remain planned:

```{r bivariate-coscale-corpairs}
corpairs(fit_biv)[
  c("level", "from_response", "to_response", "class", "parameter",
    "estimate", "min", "max", "modelled")
]
```

The `rho12` coefficients are fitted on the linear-predictor scale
`eta_rho12`; the response transform is
`rho12 = tanh(eta_rho12)` with the tiny internal guard described above. Use
`rho12(fit)` to read them as residual correlations, and use `corpairs(fit)` to
place residual `rho12` in a table that is ready to hold other correlation
levels later. In this example, a positive coefficient for `disturbance` means
that observations with larger disturbance have stronger positive residual
coupling between activity and boldness, after the model has already accounted
for the predictors in `mu1`, `mu2`, `sigma1`, and `sigma2`.

Keep the three correlation words separate:

| Word | Use it for | Do not use it for |
|---|---|---|
| `rho12` | the residual bivariate Gaussian coscale formula and extractor | group-level, phylogenetic, spatial, animal-model, or known-matrix covariance |
| `corpair()` | a formula for an implemented predictor-dependent latent random-effect pair, currently selected q=2 ordinary and phylogenetic location-location routes | residual correlation or q=4, spatial, animal, `relmat()`, residual-scale, and slope-specific correlation regressions |
| `corpairs()` | extracting fitted residual and latent correlation rows from the model object | asking the model to fit a new covariance structure |

A location-scale model changes `sigma`; a location-coscale model changes
residual `rho12`. A double-hierarchical or structured covariance model may
also report latent correlations through `corpairs()`, but those rows answer
questions about group, phylogenetic, spatial, animal-model, or known-matrix
deviations rather than residual coupling within an observation.

```{r bivariate-coscale-interpretation-table}
newdat <- data.frame(
  food = 0,
  temperature = 0,
  disturbance = c(-1, 0, 1)
)
rho_table <- data.frame(
  disturbance = newdat$disturbance,
  sigma_activity = predict(fit_biv, newdata = newdat, dpar = "sigma1"),
  sigma_boldness = predict(fit_biv, newdata = newdat, dpar = "sigma2"),
  rho12 = rho12(fit_biv, newdata = newdat)
)
rho_table$residual_covariance <- with(
  rho_table,
  rho12 * sigma_activity * sigma_boldness
)
rho_table$residual_variance_activity <- rho_table$sigma_activity^2
rho_table$residual_variance_boldness <- rho_table$sigma_boldness^2
round(rho_table, 3)
```

This table gives three quantities a reader can report directly. `sigma_activity`
and `sigma_boldness` are the fitted residual SDs for each response at the
chosen covariate values. `rho12` is the fitted residual correlation between the
two responses. `residual_covariance` is the off-diagonal element of
`\Omega_i`, computed as `rho12 * sigma_activity * sigma_boldness`. The two
residual variance columns are the diagonal elements of `\Omega_i`.

## What should I report?

For a location-coscale model, a useful result sentence usually needs all three
distributional pieces: means, residual SDs, and residual coupling. A minimal
table for the coscale part is:

```{r bivariate-coscale-report-table}
report_grid <- data.frame(
  food = 0,
  temperature = 0,
  disturbance = c(-1.5, 0, 1.5)
)
report_table <- data.frame(
  disturbance = report_grid$disturbance,
  eta_rho12 = predict(
    fit_biv,
    newdata = report_grid,
    dpar = "rho12",
    type = "link"
  ),
  rho12 = rho12(fit_biv, newdata = report_grid),
  sigma_activity = predict(fit_biv, newdata = report_grid, dpar = "sigma1"),
  sigma_boldness = predict(fit_biv, newdata = report_grid, dpar = "sigma2")
)
report_table$residual_covariance <- with(
  report_table,
  rho12 * sigma_activity * sigma_boldness
)
report_table$residual_variance_activity <- report_table$sigma_activity^2
report_table$residual_variance_boldness <- report_table$sigma_boldness^2
round(report_table, 3)
```

The raw correlation between activity and boldness is not the same estimand as
`rho12`. The raw correlation mixes mean structure, residual SDs, and residual
coupling. The fitted `rho12` is the residual correlation after the model has
accounted for the formulae in `mu1`, `mu2`, `sigma1`, and `sigma2`:

```{r bivariate-coscale-raw-vs-residual}
round(data.frame(
  raw_activity_boldness_correlation = stats::cor(dat$activity, dat$boldness),
  mean_fitted_residual_rho12 = mean(rho12(fit_biv))
), 3)
```

When reporting this model, avoid saying only "activity and boldness were
correlated". A better sentence is: "After modelling response-specific means
and residual SDs, the fitted residual activity-boldness correlation increased
with disturbance." That sentence tells the reader which correlation has been
modelled.

The same response-scale result can be visualised as a `rho12` curve. Here the
ribbon is a 95% Wald interval from `predict_parameters()`. A predictor-dependent
`rho12` has no single scalar target, so you must supply `newdata` and the
interval is computed for each supplied row;
`confint(..., parm = "rho12", newdata = ..., method = "profile")` gives the
profile counterpart. Treat both as computed but not coverage-certified: no
simulation study yet establishes their nominal coverage for a regression
`rho12`. Fit `rho12 ~ 1` when you want the constant residual-correlation profile
interval. That interval is finite and can be reported, but its coverage is not
certified either: the current ledger has no committed bivariate fixed-effect
CI-coverage study for this constant `rho12` cell.

```{r bivariate-coscale-rho12-curve, fig.width = 6, fig.height = 4, fig.cap = "Fitted residual `rho12` over disturbance after modelling response-specific means and residual SDs; the ribbon is a 95% Wald confidence interval computed for each supplied row (coverage not certified) and the dotted line marks zero residual correlation.", fig.alt = "Line plot of fitted residual rho12 correlation over disturbance with a pale 95 percent Wald confidence ribbon and a dotted zero line, increasing from negative to positive values and crossing zero near average disturbance."}
rho_grid <- data.frame(
  food = 0,
  temperature = 0,
  disturbance = seq(-2, 2, length.out = 80)
)
rho_pred <- predict_parameters(
  fit_biv,
  newdata = rho_grid,
  dpar = "rho12",
  conf.int = TRUE
)
rho_grid$rho12 <- rho_pred$estimate
rho_grid$conf.low <- rho_pred$conf.low
rho_grid$conf.high <- rho_pred$conf.high

if (requireNamespace("ggplot2", quietly = TRUE)) {
  ggplot2::ggplot(rho_grid, ggplot2::aes(disturbance, rho12)) +
    ggplot2::geom_hline(yintercept = 0, linetype = "dotted", colour = "grey60") +
    ggplot2::geom_ribbon(
      ggplot2::aes(ymin = conf.low, ymax = conf.high),
      fill = "#006D77",
      alpha = 0.18,
      colour = NA
    ) +
    ggplot2::geom_line(linewidth = 0.9, colour = "#006D77") +
    ggplot2::coord_cartesian(ylim = c(-1, 1)) +
    ggplot2::labs(
      x = "Disturbance",
      y = "Fitted residual correlation (rho12)",
      title = "Residual coupling changes with disturbance",
      subtitle = "Ribbon is a 95% Wald interval; dotted line marks rho12 = 0"
    ) +
    ggplot2::theme_minimal(base_size = 11) +
    ggplot2::theme(
      panel.grid.minor = ggplot2::element_blank(),
      plot.title = ggplot2::element_text(face = "bold"),
      plot.subtitle = ggplot2::element_text(colour = "grey30"),
      plot.background = ggplot2::element_rect(fill = "white", colour = NA),
      panel.background = ggplot2::element_rect(fill = "white", colour = NA)
    )
} else {
  old_par <- graphics::par(bg = "white")
  on.exit(graphics::par(old_par), add = TRUE)
  plot(
    rho12 ~ disturbance,
    data = rho_grid,
    type = "l",
    lwd = 2,
    ylim = c(-1, 1),
    xlab = "Disturbance",
    ylab = "Fitted residual correlation (rho12)"
  )
  abline(h = 0, lty = 3, col = "grey60")
}
```

In this example, a positive `disturbance` coefficient for `rho12` means that
activity and boldness become more tightly positively coupled at higher
disturbance, after their means and residual SDs have been modelled. It does
not say that disturbance changes among-individual personality or plasticity
correlations; those require group-level covariance blocks and should appear as
separate rows in `corpairs(fit)`.

A concise reporting sentence would be: after accounting for food,
temperature, and response-specific residual SDs, the fitted residual
correlation between activity and boldness increased along the disturbance
gradient. In biological terms, individuals that were more active than expected
were also more bold than expected under higher disturbance.

## Where residual rho12 stops

The fitted model above has no repeated-individual structure. It estimates
residual coupling: whether two measurements from the same observation are
closer together or farther apart than expected after modelling their means and
residual SDs. That is useful, but it is not the same question as whether some
individuals have consistently high activity and consistently high boldness.

For ecological and evolutionary examples, residual `rho12` can represent
changing residual coupling between traits such as activity and boldness, body
size and fecundity, or leaf area and seed number. A group-level covariance
block asks a different question: do groups, individuals, species, or sites
share latent deviations across fitted components?

## Worked example: individual differences

Suppose the same ecologist repeatedly measures each individual and now asks an
individual-difference question. After accounting for food and disturbance, do
individuals with higher average activity also tend to have higher average
boldness?

The first fitted group-level version adds matching labelled random intercepts
in the mean structure. The shared `p` label creates one covariance block for
the `mu1` and `mu2` random intercepts:

```r
drmTMB(
  formula = bf(
    mu1 = activity ~ food + disturbance + (1 | p | ID),
    mu2 = boldness ~ food              + (1 | p | ID),
    sigma1 = ~1,
    sigma2 = ~1,
    rho12 = ~1,
    corpair(ID, level = "group", block = "p", from = "mu1", to = "mu2") ~ 1
  ),
  family = biv_gaussian(),
  data = dat_group
)
```

Here `sigma1` and `sigma2` are residual scale parameters. Read the fitted
group-level random-intercept SDs from `summary(fit)$parameters` or their named
rows in `profile_targets(fit)`; they are not residual `sigma`.

Symbolically, the implemented random-intercept mean part is:

\[
\begin{aligned}
\mu_{1ij}
  &= X_{\mu 1}[ij, ]\beta_{\mu 1} + b_{0,1j},\\
\mu_{2ij}
  &= X_{\mu 2}[ij, ]\beta_{\mu 2} + b_{0,2j},\\
\begin{bmatrix}
b_{0,1j}\\
b_{0,2j}
\end{bmatrix}
  &\sim \operatorname{MVN}(\mathbf{0}, \Sigma_{\mu,ID}).
\end{aligned}
\]

The covariance matrix `Sigma_mu_ID` contains two group-level standard
deviations and one group-level correlation. That correlation answers whether
individuals with higher average activity also tend to have higher average
boldness. It is not `rho12_i`. The parameter `rho12_i` remains the
observation-level residual correlation in `Omega_i`.

The small simulation below creates repeated observations from that model:

```{r bivariate-coscale-group-simulate}
set.seed(98)
n_ID <- 55
n_each <- 7
ID <- factor(rep(seq_len(n_ID), each = n_each))
id_index <- as.integer(ID)
n <- length(ID)

food <- rnorm(n)
disturbance <- rnorm(n)

sigma_activity <- 0.35
sigma_boldness <- 0.45
sd_activity_ID <- 0.65
sd_boldness_ID <- 0.65
rho_ID <- 0.55
rho_residual <- 0.15

u_activity <- rnorm(n_ID)
u_boldness <- rho_ID * u_activity + sqrt(1 - rho_ID^2) * rnorm(n_ID)

e_activity <- rnorm(n)
e_boldness <- rho_residual * e_activity +
  sqrt(1 - rho_residual^2) * rnorm(n)

dat_group <- data.frame(ID = ID, food = food, disturbance = disturbance)
dat_group$activity <- 0.25 + 0.45 * food + 0.15 * disturbance +
  sd_activity_ID * u_activity[id_index] + sigma_activity * e_activity
dat_group$boldness <- -0.15 + 0.35 * food +
  sd_boldness_ID * u_boldness[id_index] + sigma_boldness * e_boldness
```

Fit the grouped model with matching `(1 | p | ID)` terms in the two location
formulas:

```{r bivariate-coscale-group-fit}
fit_group <- drmTMB(
  bf(
    mu1 = activity ~ food + disturbance + (1 | p | ID),
    mu2 = boldness ~ food + (1 | p | ID),
    sigma1 = ~1,
    sigma2 = ~1,
    rho12 = ~1,
    corpair(ID, level = "group", block = "p", from = "mu1", to = "mu2") ~ 1
  ),
  family = biv_gaussian(),
  data = dat_group,
  control = list(eval.max = 500, iter.max = 500)
)
```

In a real analysis, inspect every row of `check_drm(fit_group)`. The filtered
rows below show the convergence and covariance checks most directly tied to
this individual-difference question:

```{r bivariate-coscale-group-check}
group_checks <- check_drm(fit_group)
group_checks[
  group_checks$check %in% c(
    "optimizer_convergence",
    "random_effect_sd_boundary",
    "rho12_boundary",
    "biv_mu_random_effect_covariance"
  ),
  c("check", "status", "value", "message")
]
```

## Reading group-level covariance

`corpairs()` keeps residual and group-level correlations in the same long table
without giving them the same meaning. The ordinary intercept-only q2 group row
is interval-feasible but not coverage-backed, so this article keeps both rows
point-only rather than presenting an unvalidated reporting interval:

```{r bivariate-coscale-group-corpairs}
pair_table <- corpairs(
  fit_group
)
pair_table[
  ,
  c(
    "level", "group", "block", "from_dpar", "to_dpar", "class",
    "parameter", "estimate", "conf.status", "interval_source", "modelled"
  )
]
```

The optional plotting helper consumes that explicit table. Faceting by `level`
keeps the residual `rho12` row visually separate from the group-level
random-intercept correlation:

```{r bivariate-coscale-group-corpairs-plot, fig.width = 6.6, fig.height = 3.4, fig.cap = "Point display separating residual `rho12` from the individual-level mean-mean random-intercept correlation; the dotted line marks zero correlation. The group row is not shown with an interval because coverage-backed validation remains planned.", fig.alt = "Point plot comparing residual rho12 correlation with the individual-level mean-mean random-intercept correlation, with hollow point estimates and a dotted zero line on a correlation scale from minus one to one."}
if (requireNamespace("ggplot2", quietly = TRUE)) {
  pair_plot_table <- pair_table
  pair_plot_table$display_label <- ifelse(
    pair_plot_table$level == "residual",
    "Residual\nrho12",
    "Individual\nmean-mean"
  )
  plot_corpairs(pair_plot_table, label = "display_label", facet = NULL) +
    ggplot2::labs(
      title = "Residual and individual-level correlations are separate",
      subtitle = "Point estimates only; dotted line marks zero correlation",
      colour = NULL
    ) +
    ggplot2::theme_minimal(base_size = 11) +
    ggplot2::theme(
      panel.grid.minor = ggplot2::element_blank(),
      plot.title = ggplot2::element_text(face = "bold"),
      plot.subtitle = ggplot2::element_text(colour = "grey30"),
      legend.position = "none"
    )
}
```

The residual row is `rho12`: within-observation activity-boldness coupling.
The group row is the `mu1`/`mu2` random-intercept correlation: among-individual
covariation in average activity and average boldness.

`summary(fit_group)$covariance` gives the same group-level row with the
component SDs and covariance on the interpretation scale:

```{r bivariate-coscale-group-covariance-table}
group_covariance <- summary(fit_group)$covariance
report_group_covariance <- group_covariance[
  ,
  c(
    "level", "group", "block", "from_response", "to_response", "class",
    "correlation", "from_sd", "to_sd", "covariance", "from_scale",
    "to_scale", "covariance_conf.status"
  )
]
numeric_columns <- vapply(report_group_covariance, is.numeric, logical(1))
report_group_covariance[numeric_columns] <- lapply(
  report_group_covariance[numeric_columns],
  round,
  3
)
report_group_covariance
```

For this mean-mean block, `from_sd` is the among-individual SD in average
activity and `to_sd` is the among-individual SD in average boldness. The
`correlation` column is the individual-level activity-boldness correlation,
and `covariance` is `correlation * from_sd * to_sd`. The `identity` scale
columns tell you these quantities are on the response-location scale, not on a
log residual-SD scale.

For this fitted block, `check_drm()` reports whether group levels are
replicated and whether either group-level SD is tiny relative to the matching
residual scale. A note there is not a proof that the model is wrong; it tells
you to inspect whether the group-level SDs and correlation are supported by the
data before treating them as biological conclusions.

Use `profile_targets(fit_group)` before requesting profile intervals for this
block. The SDs are direct random-effect targets, while the modelled
`corpair()` row is a fixed-effect target on the correlation-link scale:

```{r bivariate-coscale-group-targets}
group_targets <- profile_targets(fit_group)
group_cor_dpar <- 'corpair(ID, level = "group", block = "p", from = "mu1", to = "mu2")'
group_target_names <- c(
  "sd:mu:mu1:(1 | p | ID)",
  "sd:mu:mu2:(1 | p | ID)",
  paste0("fixef:", group_cor_dpar, ":(Intercept)")
)
group_targets[
  match(group_target_names, group_targets$parm),
  c("parm", "profile_ready", "profile_note")
]
```

The group-level SD targets are named like `sd:mu:mu1:(1 | p | id)` and
`sd:mu:mu2:(1 | p | id)`. The modelled group-level correlation target begins
with `fixef:corpair(...)`. A `confint(..., newdata = ...)` call can diagnose its
response-scale profile geometry, but that callable target is not
coverage-backed interval evidence and was not used in the point-only plot.
These targets are separate from residual `rho12`. Returned diagnostic profile
rows include `profile.boundary` and `profile.message`; a non-`"ok"` message is a
prompt to inspect `check_drm(fit)` and the profile path.

## First slope-slope covariance slice

The first ordinary bivariate random-slope route asks a narrower
plasticity-syndrome question: do individuals with steeper response-1 slopes
also tend to have steeper response-2 slopes for the same predictor? It is a
group-level covariance question, not residual `rho12`.

For a shared predictor `x`, the fitted mean layer is:

\[
\begin{aligned}
\mu_{1ij}
  &= X_{\mu 1}[ij, ]\beta_{\mu 1} + x_{ij}b_{1j},\\
\mu_{2ij}
  &= X_{\mu 2}[ij, ]\beta_{\mu 2} + x_{ij}b_{2j},\\
\begin{bmatrix}
b_{1j}\\
b_{2j}
\end{bmatrix}
  &\sim \operatorname{MVN}(\mathbf{0}, \Sigma_{\mu,x,ID}).
\end{aligned}
\]

The matching first-slice syntax uses slope-only labelled blocks in both
location formulas:

```r
drmTMB(
  bf(
    mu1 = y1 ~ x + (0 + x | p | id),
    mu2 = y2 ~ x + (0 + x | p | id),
    sigma1 = ~1,
    sigma2 = ~1,
    rho12 = ~1
  ),
  family = biv_gaussian(),
  data = dat
)
```

Read this model through the named SD rows in `summary(fit)$parameters` for the
two response-specific slope SDs, `corpairs(fit, class = "slope-slope")` for
the group-level slope-slope correlation, `summary(fit)$covariance` for the covariance table, and
`profile_targets(fit)` for the direct SD and correlation target names. Run
`check_drm(fit)` before interpreting the row; the relevant diagnostic is the
bivariate `mu` random-effect covariance check, plus the ordinary convergence,
Hessian, and boundary checks.

This slope-only route deliberately excludes random intercepts from the same
labelled bivariate block. If the scientific question needs baseline
individual differences and slope differences in the same two-response block,
matching q4 and q6 location blocks are now source-tested with syntax such as
`(1 + x | p | id)` or `(1 + x + z | p | id)` in both location formulas. Treat
those routes as fitted extractor/profile-target slices rather than a full
tutorial or broad simulation claim. Matching q2 `sigma1`/`sigma2` scale-slope
blocks and same-response q2 `mu`/`sigma` slope covariance are now fitted as
their own routes. The first ordinary q8 all-endpoint block has opt-in
smoke/recovery artifact tasks; broader p8/q8 variants and coverage or power
claims remain planned.

## Boundaries for this covariance layer

The middle `p` in `(1 | p | ID)` follows grouped covariance-block syntax for
correlated group-level effects. This syntax is distinct from residual `rho12`.
Use `rho12(fit)` for within-observation residual coupling and
`corpairs(fit, level = "group")` for fitted ordinary group-level covariance
rows.

This example teaches only the `mu1`/`mu2` random-intercept covariance row.
Other implemented ordinary bivariate random-intercept rows include
`sigma1`/`sigma2`, one same-response `mu`/`sigma` pair, and the all-four
`mu1`/`mu2`/`sigma1`/`sigma2` block. Read the all-four block as six rows: one
`mu1`-`mu2` row, four mean-scale rows (`mu1`-`sigma1`, `mu1`-`sigma2`,
`mu2`-`sigma1`, and `mu2`-`sigma2`), and one `sigma1`-`sigma2` row.

The first ordinary bivariate slope-slope route is now the matching
`(0 + x | p | id)` block in both `mu1` and `mu2`. The first ordinary q8
all-endpoint block is available as a diagnostic artifact lane, not a tutorial
or coverage claim. Broader p8/q8 location-scale slope variants, random effects
in `rho12`, bivariate `meta_V()` plus random effects,
mixed-response families, and ordinary spatial group-level covariance remain
future work unless a later status map says otherwise. For phylogenetic
covariance, use the structural-dependence tutorial and keep those rows
separate from both ordinary group-level covariance and residual `rho12`.
