Skip to content

lotri

1.0.5Package record

A Simple Way to Specify Symmetric, Block Diagonal Matrices

8packages dependsuggested by 2
30.4Kdownloads / year
–test coverage
13/13flavors OK

Overview

About

Provides a simple mechanism to specify a symmetric block diagonal matrices (often used for covariance matrices). This is based on the domain specific language implemented in 'nlmixr2' but expanded to create matrices in R generally instead of specifying parts of matrices to estimate. It has expanded to include some matrix manipulation functions that are generally useful for 'rxode2' and 'nlmixr2'.

Changes in 1.0.5

New features

  • A block can now be repeated with same(), which is NONMEM's $OMEGA BLOCK(n) SAME:

```r

lotri({

iov.cl1 + iov.v1 ~ c(0.1,

0.01, 0.2)

iov.cl2 + iov.v2 ~ same()

iov.cl3 + iov.v3 ~ same()

})

```

This is one estimated 2x2 covariance shared by three blocks, which is

Show all changes in 1.0.5Hide the rest of the changes in 1.0.5

how inter-occasion variability is parameterized when every occasion

draws its own random effects from one shared covariance -- and it is

what lets those random effects be *correlated*.

same() repeats the immediately preceding block under new names and

takes no arguments. A further same() repeats that same original

block rather than the copy, the way NONMEM chains SAME. It works

with a condition (iov.cl2 + iov.v2 ~ same() | occ), it inherits the

fixed flags of the block it repeats, and it composes with the

cnd(same = n) nesting property that lotriSep() uses.

  • **The condition column can now carry a :same: suffix.** Rather than adding a column to the data frame from as.data.frame(), a repeated block records the element it mirrors in the existing condition column, naming it: `` <baseCondition>:same:<masterEta> # diagonal row <baseCondition>:same:<masterEta1>:<masterEta2> # covariance row ` The master is named rather than indexed because neta1/neta2 are renumbered whenever parameters are added, dropped or reordered. A copy keeps its master's est and fix, so a consumer that does not understand the suffix still sees a numerically correct matrix -- just with more free parameters than the model really has. Four helpers are exported for reading that column, and code that compares condition directly (condition == "id"`) should move to them, since such a test misclassifies a repeated block:
  • lotriBaseCondition() strips the suffix,
  • lotriIsSame() says whether a row mirrors another,
  • lotriSameMap() maps each eta to the eta it mirrors,
  • lotriSameBreak() drops the linkage for a block that has been structurally changed.

same() round trips through as.data.frame(), as.lotri() and

as.expression(), and the linkage is carried across lotriMat().

A repetition that no longer describes a real mirror -- because the

blocks it was built from were reordered or dropped, or because it

carries something same() cannot express -- is written out with its

explicit values instead. That loses the annotation, never the

numbers.

A prior cannot be put on a repeated block -- it is not a parameter of

its own, so the prior belongs on the block it mirrors. Without this

a model could carry two different priors on what is one estimated

parameter.

same() cannot be combined with a cov function, nor with

rcm=TRUE on a matrix the permutation would actually reorder; both

would move a repeated block away from the block it repeats, and are

refused rather than silently producing a matrix whose repetition is

no longer true. A same() matrix that is already block diagonal is

left alone, since there rcm is a no-op, which keeps rxode2's

ini({}) -- which always passes rcm=TRUE -- working.

  • Prior distributions can now be specified in a lotri({}) (and therefore ini({})) block with prior(name) ~ dist(...), ie:

```r

lotri({

tka <- 0.45

tcl <- c(0, 1, 10)

eta.cl + eta.v ~ c(0.1,

0.01, 0.2)

prior(tka) ~ dnorm(0, 10)

prior(tcl) ~ dlnorm(1, 0.5)

prior(eta.cl, eta.v) ~ lkjCorr(2)

})

```

Because the statement names its target, prior lines may be given

anywhere in the block. Priors may be put on population estimates,

on individual etas, and on whole covariance blocks (with the

matrix-valued distributions like lkjCorr() and invWishart()).

Every distribution has three accepted spellings: the R name where R

parameterizes it the same way 'Stan' does (dnorm(), dlnorm(),

dgamma()), the camelCase name (invWishart(), lkjCorr(),

studentT()), and the 'Stan' name itself (inv_wishart(),

lkj_corr(), student_t()). The canonical spelling, which is what

is stored and printed back, is the R one where there is a faithful

one and the camelCase one otherwise. Both positional and named

arguments work. lotri validates the distribution name, its arity,

and its support against the parameter's bounds; it does not generate

any 'Stan' code.

  • An inverse Wishart prior on an omega block can be given by its degrees of freedom alone, since the block it is put on already is the scale matrix. This is the $OMEGAP/$OMEGAPD pair of a NONMEM NWPRI model:

```r

lotri({

eta.cl + eta.v ~ c(0.1,

0.01, 0.2)

prior(eta.cl, eta.v) ~ invWishart(4)

})

```

It works on a 1x1 block too (an inverse Wishart of dimension one is

an inverse gamma), and an improper nu <= p - 1 is an error.

  • A normal prior can be put on the omega values themselves, which is what a NONMEM TNPRI model needs. om. prepended to a between subject variability names its omega element, so the normal prior shorthand can be used on it:

```r

lotri({

eta.cl ~ 0.3

eta.v ~ 0.1

om.eta.cl ~ 0.01

om.eta.v ~ 0.04

})

```

The omega itself is untouched; only the prior is added. Correlated

omega priors work the same way, including the per row line form. An

om. name has to match a real between subject variability and never

creates one. prior(om.eta.cl) and prior(eta.cl) mean the same

thing; the om. spelling exists so the shorthand has a name to put

on the left of a ~, since eta.cl ~ ... already means the omega

value.

Degrees of freedom and a normal prior are alternative ways of putting

a prior on an omega, so a model that gives both is an error.

  • Because normal priors are so common they also have a shorthand that reuses the matrix syntax: putting a *population estimate* on the left of a ~ gives it a normal prior with a zero mean and the given variance.

```r

lotri({

tka <- 1

tcl <- 3

tv <- 4

tka ~ 4 # tka ~ N(0, sd=2)

tcl + tv ~ c(1, # (tcl, tv) ~ MVN(0, Sigma)

0.01, 1)

})

```

All of the matrix spellings work here, including the per row line form

(tcl ~ 1; tv ~ c(0.01, 1)) and the sd(), var(), cor(), cov()

and chol() transformations. The <- value remains the initial

estimate; it is not the prior mean. An uncorrelated block simply

becomes independent normal priors. A zero variance is an error.

Note this changes behavior: lotri({b <- 3; b ~ 0.4}) used to be a

"duplicated parameter" error and is now a normal prior on b. A name

that is not an estimate still specifies an eta as before.

  • Added lotriPriorDists() which returns the table of supported distributions (including the 'Stan' name for each), so that downstream packages can generate the corresponding 'Stan' code.
  • The lotriEst data frame and as.data.frame() output gained a prior column.
  • prior() also takes the normal prior shorthand, so prior(tka) ~ 0.1 means what tka ~ 0.1 means:

```r

lotri({

tka <- 0.45

prior(tka) ~ 0.1 # dnorm(0.45, sqrt(0.1))

prior(tcl, tv) ~ c(1, # multivariate, centered on the estimates

0.01, 1)

})

```

Every matrix spelling works here as it does bare, including the per

row line form, so a covariance can be built up a line at a time:

```r

lotri({

tcl <- 3

tv <- 4

prior(tcl) ~ 1

prior(tv) ~ c(0.001, 1) # same 2x2 as tcl ~ 1; tv ~ c(0.001, 1)

})

```

An uncorrelated group becomes independent normal priors, and the mean

is what the model already says. Note the line form is the one place a

prior line is *not* order independent, since a row leans on the line

before it. The point is that a bare ~ cannot be used

everywhere -- piping onto a model reads tka ~ 0.1 as changing the

estimate -- so the prior() flag gives the shorthand a spelling that

works when piping too.

A namespaced distribution such as stats::dnorm(0, 1) is now an

error rather than being evaluated as a variance.

  • A univariate prior (dnorm(), dcauchy(), ...) can now be put on a single omega COVARIANCE (off-diagonal) element, not just a diagonal one -- a marginal, independent prior on that one cell, distinct from a whole-block distribution like invWishart()/multiNormal():

```r

lotri({

eta.cl + eta.v ~ c(0.1,

0.01, 0.2)

prior(eta.cl, eta.v) ~ dnorm(0, 0.1)

})

```

The two names must covary with each other but need not be the model's

*entire* connected block -- prior(eta.cl, eta.v) works even when a third,

correlated eta is also in the block, unlike a whole-block prior which

still requires naming every member. prior(om.eta.cl, om.eta.v) ~ ...

is accepted identically. A block cannot carry both a whole-block

invWishart()/multiNormal() prior and a marginal prior on one of its

own cells -- these remain alternatives, not additions.

Bug fixes

  • A condition written on a continuation no longer relocates unrelated parameters. The rows of one block share a level of variability because they covary, so a condition on a later row carries that block over -- but it used to carry the whole default level with it, so parameters declared long before, and unrelated to the block, silently changed level.
  • A line that cannot be parsed at the default level is no longer folded into the most recently *seen* condition. It is folded only when the block immediately before it is at that level, which is what the fallback was for; otherwise a parameter with no condition could land at a level of variability it was never given.
  • Labels are no longer lost, or attached to the wrong parameter, when a block has more than one level of variability. Which level a trailing label() belonged to was decided by whether the DEFAULT level had any labels yet, so once it did, a conditioned line's label landed on the default level and overwrote the one already there; and as.data.frame() only ever read the labels of a single matrix, so a conditioned object lost them entirely.
  • as.lotri() on a data frame whose only level of variability is not id now keeps that level's name. It previously returned a bare matrix, so an occasion-only model came back looking like an id level one.
  • lotriEst(x, drop=TRUE) kept the lotriFix class only when there were fixed elements, so a matrix carrying labels, priors or a same() repetition came back unclassed with the attribute orphaned and every consumer dispatching to the default method.
  • A plus-form block, or a block whose right hand side is arithmetic (d ~ 0.1*2), written after a line-form block overwrote the rows the line form had just written. A line-form block leaves the row counter pointing at its FIRST row with the rest held separately, and neither branch settled that, so lotri({a ~ 1; b ~ c(0.1, 2); c1 + d1 ~ c(3, 0.2, 4)}) failed with "length of 'dimnames' [1] not equal to array extent". The same applied to a 1x1 block under a condition.
  • A conditioned block written in the line form lost every row after the first. a ~ 1 | occ did not set the lastN counter the line form accumulates through, so a following b ~ c(0.1, 2) | occ could not see the row before it and the block was silently truncated to 1x1. This showed up as eval(as.expression(x)) returning a smaller matrix than x for any conditioned block of more than one parameter.
  • lotri() read its own condition-property list with a partially-matching attr(). Because "lotri" is a prefix of "lotriLabels", "lotriFix" and friends, combining a conditioned matrix with a labelled one (as in lotri(lotri(b ~ 2) | occ, lotri({c ~ 1; label("z")}))) picked up the neighbouring attribute, attached it as the property list, and gave the result a spurious lotri class and a bogus Properties: line. The internal reads are now exact.
  • lotri now requires 'armadillo4r' 15.4.2 or newer. The 'Armadillo' headers shipped with older 'armadillo4r' releases discard the return value of a std::uniform_int_distribution draw, which the 'libc++' shipped with 'clang' 23 now marks [[nodiscard]]. That made R CMD check report a significant compilation warning -- and so a check WARNING -- on CRAN's clang-trunk flavour, even though none of lotri's own sources were involved (#57).
  • src/nearPD.cpp and src/rcm.cpp now include <algorithm>, <cmath> and <cstddef> for the std::fill/std::fill_n, std::max, std::abs and std::size_t they use directly, instead of relying on the 'Armadillo' headers to drag them in. 'libc++' has been steadily dropping transitive includes, and clang 23's is where that started breaking packages (#57).
  • The trace=TRUE iteration message in lotriNearPD() printed an arma::uword with %lld; it now uses %llu with an explicit cast, so the format matches the argument on every 'Armadillo' word-size configuration (#57).
  • The test suite no longer calls structure() with the deprecated special names .Dim, .Dimnames and .Names; it uses dim, dimnames and names instead. This clears the "Found calls to structure() using deprecated special names" NOTE on the r-devel check flavours (#57).
  • A prior() given for a fixed (fix()ed) parameter is now an error at resolution time, naming the parameter, instead of parsing and building cleanly and only surfacing as a problem in whatever consumes the priors. A fixed parameter is a constant, so it cannot carry a prior; this applies to a univariate prior row, a member of a multivariate prior, and an omega-block member on a fixed omega element alike, and also to a block prior (lkjCorr(), invWishart()) whose block has a fixed covariance even if every variance in it is free. The implicit ~invWishart(4) shorthand, which applies to every omega block in the model, quietly skips a block that is entirely fixed instead (#52).
  • lotri() can now re-parse the named c() form that lotriAsExpression()/as.expression() writes for a matrix above nameEst's size threshold when a later row's names reach back into an earlier, already-closed block -- as happens for a combined theta+omega covariance matrix, where every omega row names all the preceding theta rows too. Previously an om.-prefixed row in that position was always misread as the om. normal prior shorthand instead of the next row of the matrix itself (#53).
  • Labels now follow the matrix when rcm=TRUE re-orders it. Previously the labels stayed in the order they were parsed in while the matrix was permuted, so they were applied to the wrong parameters.
  • lotriLabels are no longer dropped when matrices are combined (ie lotri(mat1, mat2) or lotriMat()); they are now concatenated in C along with the matrix itself.
  • as.expression() now works on a lotri object that has only population estimates and no matrix; it used to fail with "second argument must be a list".
  • lotriNearPD(x, only.values=TRUE) no longer fails with "unknown c++ error" when x is already positive definite. The eigenvalues were only returned from inside the branch that clamps a negative eigenvalue, so an input that needed no clamping fell through and tried to assign the full matrix into a length-n result.
  • Fixed rchk issues and small bugs found while linting

From NEWS.md in the release

Install

Health

CRAN checks
13OK
Slowest check: 3.9 min · r-devel-windows-x86_64
Status changes
  • OK13 OK2026-09-17
  • NOTE11 OK, 2 NOTE2026-07-16
  • OK14 OK2026-03-10
Advisories

none known

Documentation

In release 1.0.5
READMEYes · 1,342 wordsVignettesYes · 3 · dynamicNEWSYes · 100% structured · mentions 1.0.5Citation fileNoWebsiteYes · pkgdown
Help pages
14 (1 internal)
Examples that run
100%12 run in a plain check
Exported functions with examples
86%
Documented parameters
98%
Return-value docs
93%
Help pages citing references
7%

Downloads

30.4K
CRAN downloads in the past year
Rank #2,386 · ~83/day · ~2.5K/mo
Daily download trend is not available in this view yet.
3K30 days
6.9K90 days
30.4K1 year
Compare downloads with other packages →
Also on1.2K r2u6 autocran23.3K conda_forge149 c2d4u

Repository

7Stars
3Forks
5Open issues
4Open PRs
9Releases
490Commits
5Commit identities
490 commits · Last activity 2026-09-11 · 0% stars, 30d
Responsiveness
How these are sampled
PRs merged (all time)
92%
Median days to close, 50 most recently updated closed issues
2
Median days to close, 50 most recently updated closed PRs
0

Stars over time

2025-08-02 · 72026-07-07 · 7
PracticesBeta

10 development-tooling and community-health practices detected across 5 families in the upstream repository

Checks run against github.com/nlmixr2/lotri on 2026-10-11.

Continuous integration (2)
GitHub ActionsR CMD check workflow

The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel

Docs source (4)
README.Rmdpkgdown

GitHub Pages site last deployed 2026-09-11 · The repository has a site deploy workflow

pkgdown site last built 2026-09-11

Coverage (1)
Coverage reporting (Codecov)
CRAN release process (2)
cran-comments.mdCRAN-SUBMISSION
Show all practicesHide 1 practice
Lint, format, editor (1)
lintr
How this is detected·Detection ruleset v3 (2026-09-26)
AI coding toolsBeta

This repository shows signs of AI coding tools.

  • Claude Codefirst seen 2026-04-02
    Seen in the repository: CLAUDE.md
    Named in .Rbuildignore: .claude, CLAUDE.md
    Pull request it wrote: opened by a project member
    Commits: at least 5 crediting it in the message
  • GitHub Copilotfirst seen 2026-04-03
    Commits: 1 by its own account
  • Antigravityfirst seen 2026-05-13
    Named in .Rbuildignore: .antigravity
From outside contributors
  • Claude Code
    Pull requests: 1 from outside contributors, first on 2026-06-15
Models named in its commits
  • Claude Sonnet 4.63 commits · Apr 2026
  • Claude Opus 4.82 commits · Jun 2026
Counts are commits, not lines of code. Commits by a tool's accounts count once across all of its accounts. Commits crediting it are the largest count among its credit lines, a minimum.How this is detected·Detection ruleset v4 (updated 2026-09-27)

Dependencies

Declared dependencies
10 external dependencies (excludes base and recommended)

Installing lotri adds 5 packages (2 with compiled code, 1.5 MB of source tarballs). Heaviest: armadillo4r 1.0 MB, checkmate 247 KB, cpp4r 203 KB.

Depends (1)
R >= 3.4.0
Imports (5)
checkmatecrayonmethodsstatsutils
LinkingTo (2)
Enhances (0)
none
Reverse dependencies
8direct
13indirect
Imports 8 · LinkingTo 2
Suggested by 2
Depends, Imports or LinkingTo, most downloaded first
Suggests, most downloaded first

People & History

People (3)
Author, Maintainer since 0.1.1 (2019)
Contributor · added in 1.0.0
Contributor · added in 1.0.3
Releases (14)

Pick two releases to compare their code metrics.

  • 1.0.5Latest
    record
    2026-09-11 · current release · diff ↗ ·
    added 5 exports
    lotriBaseCondition, lotriIsSame, lotriPriorDists, lotriSameBreak, lotriSameMap
  • 1.0.4
    record
    2026-05-14 · diff ↗
  • R
    R 4.6.0 released · 2026-04-24
  • 1.0.3
    record
    2026-03-11 · diff ↗
  • 1.0.2
    record
    2025-08-29 · diff ↗
  • 1.0.1
    record
    2025-07-18 · diff ↗
  • R
    R 4.5.0 released · 2025-04-11
  • 1.0.0
    record
    2024-09-18 · diff ↗ ·
    added 5 exports
    .lotriPointers, lotriAsExpression, lotriIsBlockMat, lotriNearPD, rcm
  • R
    R 4.4.0 released · 2024-04-24
  • R
    R 4.3.0 released · 2023-04-21
  • 0.4.3
    record
    2023-03-20 · diff ↗
  • 0.4.2
    record
    2022-06-18 · diff ↗
  • R
    R 4.2.0 released · 2022-04-22
  • 0.4.1
    record
    2022-04-21 · diff ↗
  • 0.4.0
    record
    2022-04-15 · diff ↗ ·
    added 3 exports
    lotriDataFrameToLotriExpression, lotriEst, lotriMatInv
  • R
    R 4.1.0 released · 2021-05-18
Show 6 earlier eventsHide 6 earlier events
  • 0.3.1
    record
    2021-01-05 · diff ↗
  • 0.2.2
    record
    2020-05-29 · diff ↗ ·
    added 3 exports
    as.lotri, lotriMat, lotriSep
  • R
    R 4.0.0 released · 2020-04-24
  • 0.2.1
    record
    2020-02-12 · diff ↗
  • 0.1.1
    record
    2019-05-14
  • R
    R 3.6.0 released · 2019-04-26

Package metadata

First published
2019-05-14
Total releases
14 / 7 yrs
License
GPL (>= 2) OSI
Minimum R
≥ 3.4.0
Download size
339 KB

Cite

Cite this package

Run in R for the authors' preferred citation:

citation("lotri")
Fidler, M. L., Denney, B., & Vargas Sepulveda, M. (2026). lotri: A Simple Way to Specify Symmetric, Block Diagonal Matrices (Version 1.0.5) [Computer software]. https://doi.org/10.32614/CRAN.package.lotri

This package has no citation file, so citation() returns this entry built from DESCRIPTION.

Cite the R Observatory

For a number measured here: a download total, a coverage figure, an archival date.

APA

Balamuta, J. J. (2026). R Observatory: Metrics for lotri version 1.0.5 [Data set]. HJJB, LLC. Data release v2026-10-18. https://doi.org/10.5281/zenodo.21843040

From data release v2026-10-18, which the citation names so these numbers can be found later. More on citing and the projects behind them.

Report a problem with this page →

Privacy choices

These apply to this browser and are stored on this device only. Nothing about your choice is sent to us.

Read the privacy policy