--- title: "Repeated blocks with same() in lotri" author: "Matthew Fidler" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Repeated blocks with same() in lotri} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(lotri) ``` # What `same()` is for NONMEM can declare an omega block and then repeat it: ``` $OMEGA BLOCK(2) 0.1 0.01 0.2 $OMEGA BLOCK(2) SAME $OMEGA BLOCK(2) SAME ``` Those three blocks are not three covariance matrices. They are *one* estimated covariance stamped three times. That is how inter-occasion variability is parameterized: every occasion draws its own random effects, but all occasions share one covariance -- and sharing one covariance is what lets the per-occasion random effects be **correlated**. `same()` is the `lotri` spelling of that: ```{r} iov <- lotri({ iov.cl1 + iov.v1 ~ c(0.1, 0.01, 0.2) iov.cl2 + iov.v2 ~ same() iov.cl3 + iov.v3 ~ same() }) iov ``` Three 2x2 blocks, one estimated 2x2: ```{r} dim(iov) ``` # The rules `same()` repeats the immediately preceding **block**, under new names, and takes no arguments. A second `same()` repeats that same original block rather than the previous copy, the way NONMEM chains `SAME`. It repeats the block *as declared*. A structural zero in the covariance does not split the block in two: ```{r} lotri({ a + b ~ c(1, 0, 1) c1 + d1 ~ same() }) ``` A repeated block inherits the fixed flags of the block it repeats, works under a condition, and works for a single parameter: ```{r} lotri({ eta.ka ~ 0.6 iov.cl1 + iov.v1 ~ c(0.1, 0.01, 0.2) | occ iov.cl2 + iov.v2 ~ same() | occ }) ``` A prior cannot be put on a repeated block. A copy is not a parameter of its own -- it *is* the block it mirrors -- so a prior on it would either duplicate the master's prior or silently contradict it: ```{r, error = TRUE} lotri({ a + b ~ c(1, 0.1, 2) c1 + d1 ~ same() prior(c1) ~ dnorm(0, 1) }) ``` Put it on the block that is actually estimated (`prior(a) ~ dnorm(0, 1)`). `same()` looks back only within one `{}` block, and only at its own level of variability. Each extra argument to `lotri()` is parsed by its own call, so this has nothing to repeat: ```{r, error = TRUE} lotri(a + b ~ c(1, 0.1, 2), c1 + d1 ~ same()) ``` Write the two lines in one `lotri({})` block instead. `same()` is refused with `rcm=TRUE` and with a `cov` function. Both reorder or adjust the whole matrix, which would move a repeated block away from the block it is supposed to repeat; erroring is better than returning a matrix whose claimed repetition is no longer true. # How it is stored: the `condition` column This is the part other packages need to know. `as.data.frame()` does **not** gain a column for `same()`. The repetition rides in the existing `condition` column, which names the element that is mirrored: ```{r} df <- as.data.frame(iov) df[, c("neta1", "neta2", "name", "est", "condition")] ``` The format is ``` :same: # diagonal row :same:: # covariance row ``` Two things are worth spelling out. **The master is named, not indexed.** `neta1`/`neta2` are renumbered whenever parameters are added, dropped or reordered, so an index would go stale silently. Names are unique within one of these data frames and survive reordering. **Covariance rows are carried too.** The off-diagonal row above points at the master's off-diagonal, so the *correlation* is shared, not just the variances. A repeated row keeps its master's `est` and `fix`. A consumer that knows nothing about the suffix therefore still reads a numerically correct matrix -- it just thinks the model has more free parameters than it does. # Reading the column Do not compare `condition` directly. A test like `condition == "id"` misses every repeated row, and `condition != "id"` wrongly treats one as a different level of variability. Use these instead: ```{r} lotriBaseCondition(df$condition) lotriIsSame(df$condition) ``` `lotriSameMap()` gives, for each eta, the index of the eta it mirrors (`0` when it is an ordinary or master eta). This is the object an estimator wants, since it says which parameters are actually free: ```{r} lotriSameMap(df) ``` When an edit structurally changes a block -- deleting one of its parameters, say -- the linkage no longer describes anything real. `lotriSameBreak()` drops it, turning the copies into ordinary independent blocks and leaving every other family alone: ```{r} broken <- lotriSameBreak(df, "iov.v2") broken$condition ``` # Round trips `same()` survives every direction: ```{r} identical(as.data.frame(as.lotri(df)), df) identical(as.data.frame(eval(as.expression(iov))), df) as.expression(iov) ``` and the linkage is carried across `lotriMat()`, so combining blocks does not silently turn a repeated block back into independently estimated parameters: ```{r} attr(lotriMat(lotriMatInv(iov)), "lotriSame") ``` # `same()` versus `cnd(same = n)` There are two spellings of the same NONMEM idea at different granularities, and they compose. - `~ same()` repeats **one block** inside a matrix, under names you choose. This is `$OMEGA BLOCK(n) SAME`. - `cnd(same = n)` repeats a **whole nesting level** `n` times, and is what `lotriSep()` sets up for `rxode2`'s nested simulation. Used together, a two-parameter correlated occasion block declared with `same()` can itself be stamped once per occasion: ```{r} nested <- lotri(lotri({ a + b ~ c(1, 0.1, 2) c1 + d1 ~ same() }) | occ(same = 3L)) dim(lotriMat(nested, format = "ETA[%d]", start = 1L)) ```