lotri 1.0.5
New features
- A block can now be repeated with
same(), which is NONMEM’s$OMEGA BLOCK(n) SAME:
This is one estimated 2x2 covariance shared by three blocks, which is 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
conditioncolumn can now carry a:same:suffix. Rather than adding a column to the data frame fromas.data.frame(), a repeated block records the element it mirrors in the existingconditioncolumn, naming it:<baseCondition>:same:<masterEta> # diagonal row <baseCondition>:same:<masterEta1>:<masterEta2> # covariance rowThe master is named rather than indexed because
neta1/neta2are renumbered whenever parameters are added, dropped or reordered. A copy keeps its master’sestandfix, 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
conditiondirectly (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 throughas.data.frame(),as.lotri()andas.expression(), and the linkage is carried acrosslotriMat(). A repetition that no longer describes a real mirror – because the blocks it was built from were reordered or dropped, or because it carries somethingsame()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 acovfunction, nor withrcm=TRUEon 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. Asame()matrix that is already block diagonal is left alone, since therercmis a no-op, which keepsrxode2’sini({})– which always passesrcm=TRUE– working. -
Prior distributions can now be specified in a
lotri({})(and thereforeini({})) block withprior(name) ~ dist(...), ie:
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/$OMEGAPDpair of a NONMEMNWPRImodel:
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
TNPRImodel needs.om.prepended to a between subject variability names its omega element, so the normal prior shorthand can be used on it:
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.
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
lotriEstdata frame andas.data.frame()output gained apriorcolumn.prior()also takes the normal prior shorthand, soprior(tka) ~ 0.1means whattka ~ 0.1means:
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:
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 likeinvWishart()/multiNormal():
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; andas.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 notidnow 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 thelotriFixclass only when there were fixed elements, so a matrix carrying labels, priors or asame()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, solotri({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 | occdid not set thelastNcounter the line form accumulates through, so a followingb ~ c(0.1, 2) | occcould not see the row before it and the block was silently truncated to 1x1. This showed up aseval(as.expression(x))returning a smaller matrix thanxfor any conditioned block of more than one parameter.lotri()read its own condition-property list with a partially-matchingattr(). Because"lotri"is a prefix of"lotriLabels","lotriFix"and friends, combining a conditioned matrix with a labelled one (as inlotri(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 spuriouslotriclass and a bogusProperties:line. The internal reads are now exact.lotrinow requires ‘armadillo4r’ 15.4.2 or newer. The ‘Armadillo’ headers shipped with older ‘armadillo4r’ releases discard the return value of astd::uniform_int_distributiondraw, which the ‘libc++’ shipped with ‘clang’ 23 now marks[[nodiscard]]. That madeR CMD checkreport a significant compilation warning – and so a checkWARNING– on CRAN’s clang-trunk flavour, even though none oflotri’s own sources were involved (#57).src/nearPD.cppandsrc/rcm.cppnow include<algorithm>,<cmath>and<cstddef>for thestd::fill/std::fill_n,std::max,std::absandstd::size_tthey 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=TRUEiteration message inlotriNearPD()printed anarma::uwordwith%lld; it now uses%lluwith 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,.Dimnamesand.Names; it usesdim,dimnamesandnamesinstead. This clears the “Found calls to structure() using deprecated special names”NOTEon 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 namedc()form thatlotriAsExpression()/as.expression()writes for a matrix abovenameEst’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 anom.-prefixed row in that position was always misread as theom.normal prior shorthand instead of the next row of the matrix itself (#53).Labels now follow the matrix when
rcm=TRUEre-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.lotriLabelsare no longer dropped when matrices are combined (ielotri(mat1, mat2)orlotriMat()); they are now concatenated in C along with the matrix itself.as.expression()now works on alotriobject 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” whenxis 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-nresult.Fixed rchk issues and small bugs found while linting
lotri 1.0.4
CRAN release: 2026-05-14
Fix unsigned integer underflow in
rcm.cpp: when called with a 0-row matrix,n-1would wrap toUWORD_MAXcausing an infinite loop; added an early-return guard forn == 0.Fix type narrowing and underflow in
nearPD.cpp: changedunsigned int ntoarma::uwordto prevent silent 32-bit truncation; addedn == 0early-return guard to prevent out-of-bounds access on the eigenvalue vector.Fix variable shadowing in
lotriProp.c: localdouble valrenamed toinValto eliminate-Wshadowwarning against the function parameter.Fix hard-coded buffer size in
matlist.h:snprintfbuffer increased from 100 to 256 bytes and size passed viasizeof(out).Use
R_xlen_tinstead ofintfor variables storingRf_length()andRf_xlength()returns acrossmatlist.h,lotriProp.c,lotriBounds.c, andlotriLstToMat.c.
lotri 1.0.2
CRAN release: 2025-08-29
- Fix for iov variables that are right next to one another (issue #37)
lotri 1.0.0
CRAN release: 2024-09-18
- Added a new way of specifying lotri matrices:
Before you could specify matrices as:
Now you can specify per row as:
This form is now the default when converting from a matrix to a lotri expression. In addition if the matrix is large enough (by default a 5x5 matrix), these would be named when changing them to an expression:
m <- lotri({
a ~ c(a=1)
b ~ c(a=0.5, b=1)
c ~ c(a=0.5, b=0.5, c=1)
d ~ c(a=0.5, b=0.5, c=0.5, d=1)
e ~ c(a=0.5, b=0.5, c=0.5, d=1,
e=1)
})This way changing to an R parsed expression will be rendered in a more human readable format.
You can change the deparsing options that are used by default with lotri with options(lotri.plusNames=TRUE) which prefers the a+b+c syntax when deparsing. Otherwise, the line format is used by default. The dimension number before naming the values in the line-format can be controlled with options(lotri.nameEst=2) or some other dimension.
New option of
covadded which check for matrix suitability for covariance matrix. Whencov=TRUE, off-diagonal elements in covariance matrices may no longer be nonzero if the diagonal value is zero (rxode2#481). This will also check tht the matrix is non-positive definite on the non-diagonal terms.covcan also be a function to allow correction of the matrix to a positive definite matrix automatically.New option of
rcm; When enabled, andlotriIsBlockMat()is not true,lotri()will permute the matrix to try to get a banded matrix using the Reverse Cuthill McKee algorithm.Change internals for
lotriso that newrxode2is no longer required to be binary linked tolotri.Add new function
rcm()which permutes the matrix to get a band matrix (if possible). This uses the Reverse Reverse Cuthill McKee (RCM) algorithm.Moved
nmNearPD()to this package and renamed tolotriNearPD(). In addition to moving, this function will now retain the dimension names.New exported function
lotriAsExpression()which has more fine control thanas.expression()and will work without converting the matrix to a lotri form. This by default uses the new line form, but can be changed back to theoption(lotri.plusNames=TRUE). Also be default it will name each element in a matrix when the dimension is above5x5. You can change that number byoption(lotri.nameEst=10)to increase it to be named above10x10. If you do not like the naming you can also disable it withoption(lotri.nameEst=TRUE), or if you always want it on you can use `option(lotri.nameEst=FALSE)
lotri 0.4.4
- Bug fix for non-standard evaluation where you take the numeric vector from the evaluating environment when using
lotri(n1+n2~omega)
lotri 0.4.3
CRAN release: 2023-03-20
- Bug fix for etas that were not named correctly for large order problems
lotri 0.4.2
CRAN release: 2022-06-18
- Bug fix for etas that are inconsistently numbered
- Add case for empty initialization block
lotri 0.4.0
CRAN release: 2022-04-15
Can convert lotri objects to data-frames similar to the internal data frame used in
nlmixr(); These can then be converted back withas.lotri()Added the ability to add estimates to a lotri object. These estimates are an attached
data.frameto the original lotri matrix. You can extract them or drop them with the functionlotriEst()Allow specifying fixed components in
lotri()matrices.Add
cov,cor,sd,var, andcholoptions for matrix specification. The final matrix will always be the covariance matrixAdd function
lotriMatInv()which takes a symmetric block matrix and converts it into a list of matrices. An sort of inverse operation oflotriMat()Add error for
lotri(~c(40))Added ability to flag
fixedandunfixedcomponents in a matrix. Currentlylotrionly supports one type.For the
lotriFixobjects, ie those created with population types of estimates and covariance estimates, allow them to be converted to an equivalent expression withas.expression()for the lotri object andlotriDataFrameToLotriExpression()for thedata.frameAdded
lotriIsBlockMat()to check to see if a matrix is in a block diagonal matrix form.
lotri 0.3.1
CRAN release: 2021-01-05
- Change errors/warnings to use
call.=FALSEor equivalent. - Refactor C code to reduce complexity
- Change C code to play nicely with
rchk - Allow
lotriMatto mix named and unnamed matrices; When mixed, an unnamed matrix will be returned.
lotri 0.2.2
CRAN release: 2020-05-29
- Bug fix for conditional matrices
- Now accessing
$lowerand$uppergives default values even if it wasn’t specified. - Can change the default conditional matrix to some other value like “id”
- Can add properties to matrix->lotri by as.lotri(matrix, lower=3, default=“id”)
- Dropped
Matriximport and addedlotriMatto create banded matrices (faster thanMatrixfor now included repeated matrices withlist(matrix, rep)). - Bug fix for default properties when both
upperandlowerbounds are specified
