R/summary.rms.s

Defines functions plot.summary.rms html.summary.rms latex.summary.rms print.summary.rms summary.rms

Documented in html.summary.rms latex.summary.rms plot.summary.rms print.summary.rms summary.rms

## ?? Why does confint call use nrp???
## Value adjusted to is irrelevant when the factor does not interact with
# other factors.  Form of factors is as follows: factor1=value1,factor2=val2:
# Values:
#	NA	: test factor, use all default settings
#	w	: adjust this factor to w when estimating effects of others
#	c(lo,hi): use range for effect (lo,hi), adjust to default value
#	c(lo,w,hi): use range (lo,hi), adjust to w.  Any of 3 can be NA.
# For categories and strata values can be character
# values that are original values before translating to factors -
# only enough letters are needed to uniquely identify the category 
# This applies to category and strata vars.  Default adjusted to is
# from second element of limits vector.
# For category factors, all comparisons to reference category are made.
# Reference category is assumed to be adjusted to value.
# est.all is T to estimate effects for all factors, not just those listed
# in ...

summary.rms <- function(object, ..., ycut=NULL,
                        est.all=TRUE, antilog, conf.int=.95,
                        abbrev=FALSE, vnames=c("names","labels"),
                        conf.type=c('individual','simultaneous'),
                        usebootcoef=TRUE,
                        boot.type=c('percentile','bca','basic'),
                        posterior.summary=c('mean', 'median', 'mode'),
                        verbose=FALSE)
{	
  obj.name <- as.character(sys.call())[2]
  at <- object$Design
  labels <- at$label

  vnames <- match.arg(vnames)
  conf.type <- match.arg(conf.type)
  boot.type <- match.arg(boot.type)
  blabel <- switch(boot.type,
                   percentile = 'bootstrap nonparametric percentile',
                   bca        = 'bootstrap BCa',
                   basic      = 'basic bootstrap')
  ## if(conf.type == 'simultaneous') require(multcomp)
  alp <- (1. - conf.int) / 2.
  posterior.summary <- match.arg(posterior.summary)

  draws <- object$draws
  bayes <- length(draws) > 0
  if(bayes && conf.type == 'simultaneous')
    stop('conf.type simultaneous does not apply to Bayesian model fits')

  isblrm <- inherits(object, 'blrm')
  partialpo <- isblrm && object$pppo > 0
  if(partialpo & (length(ycut) != 1))
    stop('must specify a single value of ycut for partial prop. odds model')
  
  pred <- function(d)
    if(isblrm) predict(object, d, type='x', ycut=ycut)
    else
      predict(object, d, type='x')
  
  assume <- at$assume.code
  if(is.null(assume)) stop("fit does not have design information")
  if(any(assume == 10))
    warning("summary.rms does not currently work with matrix factors in model")
  name  <- at$name
  parms <- at$parms

  scale <- object$scale.pred
  if(missing(antilog)) antilog <- length(scale)==2
  if(antilog & length(scale) < 2) scale <- c("","Antilog")

  factors <- rmsArgs(substitute(list(...)))
  nf <- length(factors)

  if(est.all) which <- (1 : length(assume))[assume != 9]
  if(nf > 0) {
    jw <- charmatch(names(factors), name, 0)
    if(any(jw == 0)) stop(paste("factor name(s) not in the design:",
             paste(names(factors)[jw == 0], collapse=" ")))
    if(!est.all) which <- jw
    if(any(assume[which] == 9))
      stop("cannot estimate effects for interaction terms alone")
  }
  
  Limval <- Getlim(at, allow.null=TRUE, need.all=FALSE)
  values <- Limval$values
  ## The next statement (9Jun98) makes limits[1:3,] keep all levels of
  ## factors.  Problem is that [.data.frame does not pass drop to []
  ## when first subscripts are specified
  oldopt <- options('drop.factor.levels')
  options(drop.factor.levels=FALSE)
  on.exit(options(oldopt))

  lims <- Limval$limits[1 : 3 , , drop=FALSE]
  
  ##Find underlying categorical variables
  ucat <- rep(FALSE, length(assume))
  for(i in (1:length(assume))[assume != 5 & assume < 8])
    ucat[i] <- name[i] %in% names(values) &&
  length(V <- values[[name[i]]]) && is.character(V)
  
  stats <- lab <- NULL
  beta <- if(bayes) coef(object, stat=posterior.summary)
          else
            object$coef
  lc <- length(beta)
  ## Number of non-slopes:
  nrp  <- if(bayes) num.intercepts(object) else num.intercepts(object, 'coef')
  nrp1 <- nrp + 1
  ## Exclude non slopes
  j <- nrp1 : lc
  beta <- beta[j]
  if(bayes) draws <- draws[, j, drop=FALSE]
  var <- vcov(object, regcoef.only=TRUE, intercepts='none')

  zcrit <- if(length(idf <- object$df.residual)) qt(1. - alp, idf)
   else qnorm(1. - alp)
  cll <- paste(signif(conf.int, 3))
  bcoef <- if(usebootcoef) object$boot.Coef
  if(length(bcoef)) bcoef <- bcoef[, nrp1 : lc, drop=FALSE]

  jf <- 0
  if(nf > 0) for(i in jw) {
    jf <- jf + 1
    z <- value.chk(at, i, factors[[jf]], 0, Limval)
    lz <- length(z)
    if(lz == 1 && !is.na(z)) lims[2, i] <-  z
    if(lz == 2) {
      if(!is.na(z[1])) lims[1, i] <- z[1]
      if(!is.na(z[2])) lims[3, i] <- z[2]
    }
    else
      if(lz == 3) lims[!is.na(z), i] <- z[!is.na(z)]
    if(lz < 1 | lz > 3) stop("must specify 1,2, or 3 values for a factor")
  }
  adj  <- lims[2,, drop=FALSE]
  isna <- sapply(adj, is.na)

  if(any(isna))
    stop(paste("adjustment values not defined here or with datadist for",
               paste(name[assume != 9][isna], collapse=" ")))
  k <- which[assume[which] %nin% c(8, 5, 10) & ! ucat[which]]
  m <- length(k)
  if(m) {
    isna <- is.na(lims[1, name[k], drop=FALSE] + lims[3, name[k], drop=FALSE])
    ##note char. excluded from k
    if(any(isna)) stop(paste("ranges not defined here or with datadist for",
                             paste(name[k[isna]], collapse=" ")))
  }
  
  xadj <- unclass(rms.levels(adj, at))
  if(m) {
    adj <- xadj
    M <- 2 * m
    odd  <- seq(1, M, by=2)
    even <- seq(2, M, by=2)
    ##Extend data frame
    for(i in 1:length(adj)) adj[[i]] <- rep(adj[[i]], M)
    
    i <- 0
    for(l in k) {
      i <- i + 1
      adj[[name[l]]][(2 * i - 1) : (2 * i)] <- lims[c(1, 3), name[l]]
    }
     xx <- pred(adj)
     xd <- matrix(xx[even,] - xx[odd,], nrow=m)
    xb <- xd %*% beta
    se <- drop((((xd %*% var) * xd) %*% rep(1, ncol(xd)))^.5)
    if(conf.type == 'simultaneous' && length(xb) > 1) {
      if(verbose) {
        cat('Confidence intervals are simultaneous for these estimates:\n')
        print(as.vector(xb))
      }
      u <- confint(multcomp::glht(object,
                        cbind(matrix(0, nrow=nrow(xd), ncol=nrp), xd),
                        df=if(length(idf)) idf else 0),
                   level=conf.int)$confint
      low <- u[, 'lwr']
      up  <- u[, 'upr']
    } else if(length(bcoef)) {
      best <- t(xd %*% t(bcoef))
      lim <- bootBCa(xb, best, type=boot.type, n=nobs(object), seed=object$seed,
                     conf.int=conf.int)
      if(is.matrix(lim)) {
        low <- lim[1,]
        up  <- lim[2,]
      } else {
        low <- lim[1]
        up <- lim[2]
      }
    } else if(bayes) {
      best <- t(xd %*% t(draws))
      lim  <- apply(best, 2, rmsb::HPDint, prob=conf.int)
      low  <- lim[1, ]
      up   <- lim[2, ]
    } else {
      low <- xb - zcrit*se
      up  <- xb + zcrit*se
    }
    lm <- as.matrix(lims[, name[k], drop=FALSE])
    stats <- cbind(lm[1,], lm[3,], lm[3,] - lm[1,], xb, se, low, up, 1)
    lab <- if(vnames=='names') name[k] else labels[k]
    if(antilog) {
      stats <- rbind(stats,
                     cbind(stats[, 1 : 3,drop=FALSE],
                           exp(xb), NA, exp(low), exp(up), 2))
      lab <- c(lab, rep(paste("", scale[2]), m))
      w <- integer(M)
      w[odd]  <- 1 : m
      w[even] <- m + (1 : m)
      stats   <- stats[w,]
      lab <- lab[w]
    }
  }
  
  for(j in 1 : length(xadj)) xadj[[j]] <- rep(xadj[[j]], 2)
  
  for(i in which[assume[which] == 5 | ucat[which]]) {
    ## All comparisons with reference category
    
    parmi <- if(ucat[i]) values[[name[i]]] else parms[[name[i]]]
    parmi.a <- if(abbrev) abbreviate(parmi) else parmi
    iref <- as.character(xadj[[name[i]]][1])
    ki <- match(iref, parmi)
    for(j in parmi) {
      if(j != iref) {
        kj <- match(j, parmi)
        adj <- xadj
        adj[[name[i]]] <- c(iref, j)
        adj <- as.data.frame(adj)
        xx <- pred(adj)
        xd <- matrix(xx[2,] - xx[1,], nrow=1)
        xb <- xd %*% beta
        se <- sqrt((xd %*% var) %*% t(xd))
        if(conf.type == 'simultaneous' && length(xb) > 1) {
          if(verbose) {
            cat('Confidence intervals are simultaneous for these estimates:\n')
            print(as.vector(xb))
          }
          u <- confint(multcomp::glht(object,
                            cbind(matrix(0, nrow=nrow(xd), ncol=nrp), xd),
                            df=if(length(idf)) idf else 0),
                       level=conf.int)$confint
          low <- u[,'lwr']
          up  <- u[,'upr']
        } else if(length(bcoef)) {
          best <- t(xd %*% t(bcoef))
          lim <- bootBCa(xb, best, type=boot.type,
                         n=nobs(object), seed=object$seed, conf.int=conf.int)
          if(is.matrix(lim)) {
            low <- lim[1,]
            up  <- lim[2,]
          } else {
            low <- lim[1]
            up  <- lim[2]
          }
        } else if(bayes) {
          best <- t(xd %*% t(draws))
          lim  <- apply(best, 2, rmsb::HPDint, prob=conf.int)
          low  <- lim[1, ]
          up   <- lim[2, ]
        } else {
          low <- xb - zcrit*se
          up  <- xb + zcrit*se
        }
        stats <- rbind(stats,cbind(ki, kj, NA,
                                   xb, se, low, up, 1))
        lab <-c(lab,
                paste(if(vnames=='names') name[i] else labels[i],
                      " - ", parmi.a[kj], ":",
                      parmi.a[ki], sep=""))
        if(antilog) {
          stats <- rbind(stats,cbind(ki, kj, NA,
                                     exp(xb),
                                     NA, exp(low), exp(up), 2))
          lab <- c(lab, paste("", scale[2]))}
      }
    }
  }
  
  dimnames(stats) <-
    list(lab, c("Low", "High",
                "Diff.", "Effect", "S.E.",
                paste("Lower", cll), paste("Upper", cll), "Type"))
  
  attr(stats, "heading") <-
    paste("             Effects              Response : ",
          as.character(formula(object))[2], sep='')
  attr(stats,"class") <- c("summary.rms", "matrix")
  attr(stats,"scale") <- scale
  attr(stats,"obj.name") <- obj.name
  interact <- at$interactions
  adjust <- ""
  if(length(interact)) {
    interact <- sort(unique(interact[interact > 0]))
    nam <- name[which[match(which, interact, 0) > 0]]
    if(length(nam)) for(nm in nam) 
      adjust <- paste(adjust, nm, "=",
                      if(is.factor(xadj[[nm]]))
                      as.character(xadj[[nm]])[1] else
                      format(xadj[[nm]][1]), " ", sep="")
  }
  attr(stats, "adjust") <- adjust
  attr(stats, "conf.type") <-
    if(length(bcoef)) blabel else if(bayes) 'HPD' else 'z'
  
  stats
}

print.summary.rms <- function(x, ..., table.env=FALSE)
{
  switch(prType(),
         latex = latex.summary.rms(x, ..., file='', table.env=table.env),
         html  = return(html.summary.rms(x, ...)),
         plain = {
  
           cstats <- dimnames(x)[[1]]
           for(i in 1 : 7) cstats <- cbind(cstats, format(signif(x[, i], 5)))
           dimnames(cstats) <- list(rep("", nrow(cstats)), 
                                    c("Factor", dimnames(x)[[2]][1 : 7]))
           cat(attr(x,"heading"), "\n\n")
           print(cstats, quote=FALSE)
           if((A <- attr(x, "adjust")) != "") cat("\nAdjusted to:", A, "\n")
           blab <- switch(attr(x, 'conf.type'),
              'bootstrap nonparametric percentile' = 
              'Bootstrap nonparametric percentile confidence intervals',
              'bootstrap BCa' = 'Bootstrap BCa confidence intervals',
              'basic bootstrap' = 'Basic bootstrap confidence intervals',
              HPD = 'Bayesian highest posterior density intervals',
              '')
           if(blab != '') cat('\n', blab, '\n', sep='')
           cat('\n')
         }
         )
  invisible()
}


latex.summary.rms <-
  function(object, 
           title=paste('summary', attr(object, 'obj.name'), sep='.'),
           table.env=TRUE, ...)
{ 

  title <- title   # because of lazy evaluation
  caption <- latexTranslate(attr(object, "heading"))
  scale <- attr(object, "scale")
  object <- object[, -8, drop=FALSE]
  rowl <- latexTranslate(dimnames(object)[[1]])
  rowl <- ifelse(substring(rowl, 1, 1) == " ",
                 paste("~~{\\it ",
                       substring(rowl,2), "}", sep=""),
                 rowl) # preserve leading blank
  rowl <- sedit(rowl, "-", "---")
  cstats <- matrix("", nrow=nrow(object), ncol=ncol(object), 
                   dimnames=dimnames(object))
  for(i in 1 : 7) cstats[,i] <- format(signif(object[, i], 5))
  ## for(i in 4 : 7) cstats[,i] <- format(round(object[, i],  2))
  cstats[is.na(object)] <- ""
  caption <- sedit(caption, "    Response","~~~~~~Response")
  cstats <- as.data.frame(cstats)
  attr(cstats,"row.names") <- rowl
  names(cstats)[3] <- "$\\Delta$"
  latex(cstats, caption=if(table.env) caption else NULL,
        title=title, rowlabel="",
        col.just=rep("r", 7), table.env=table.env, ...)
}

html.summary.rms <- function(object, digits=4, dec=NULL,...) { 

  caption <- attr(object, "heading")
  ## scale   <- attr(object, "scale")
  object <- object[, -8, drop=FALSE]
  rowl <- dimnames(object)[[1]]
  rowl <- ifelse(substring(rowl, 1, 1) == " ",
                 paste("&emsp;<em>",
                       substring(rowl, 2), "</em>", sep=""),
                 rowl) # preserve leading blank
  rowl <- sedit(rowl, "-", "---")
  cstats <- matrix("", nrow=nrow(object), ncol=ncol(object), 
                   dimnames=dimnames(object))
  for(i in 1 : 7)
    cstats[,i] <- if(length(dec)) format(round(object[, i], dec))
                  else
                    format(signif(object[, i], digits))
  cstats[is.na(object)] <- ""
  caption <- sub('^ *', '', caption)
  ## htmlTable creates invalid html if start caption with blank
  caption <- sub('    Response : ', '&emsp;&emsp;Response: <code>', caption)
  caption <- paste0(caption, '</code>')
  cstats <- as.data.frame(cstats)
  attr(cstats,"row.names") <- rowl
  names(cstats)[3] <- "&Delta;"

  rendHTML(
    htmlTable::htmlTable(cstats, caption=caption,
                         ## css.cell = 'min-width: 6em;',
                         css.cell=c('', rep('padding-left:4ex;', ncol(cstats))),
                         rowlabel='', align='r', align.header='r',
                         escape.html=FALSE) )
}


## plot is not using bootstrap percentile or Bayesian HPD
## intervals but is using SE-based CLs

# was q=c(.7, .8, .9, .95, .99)
plot.summary.rms <-
  function(x, at, log=FALSE, 
           q=c(0.9, 0.95, 0.99), xlim, nbar, cex=1, nint=10,
           cex.main=1, clip=c(-1e30, 1e30), main,
           col=rgb(red=.1, green=.1, blue=.8, alpha=c(.1,.4,.7)),
           col.points=rgb(red=.1, green=.1, blue=.8, alpha=1),
           pch=17, lwd=if(length(q) == 1) 3 else 2 : (length(q) + 1),
           digits=4, declim=4, ...)
{
  isbase <- Hmisc::grType() == 'base'
  pp <- plotlyParm   # in Hmisc

  scale  <- attr(x, "scale")
  adjust <- attr(x, "adjust")
  if(adjust != '') adjust <- paste("Adjusted to:", adjust, sep="")

  Type   <- x[, "Type"]
  x      <- x[Type==1,, drop=FALSE]
  lab    <- dimnames(x)[[1]]
  effect <- x[, "Effect"]
  se     <- x[, "S.E."]
  cond <- if(isbase) ! log && any(Type == 2) else any(Type == 2)
  if(cond) {
    fun  <- exp
    tlab <- scale[2]
  }
  else {
    fun <- function(x) x
    if(log) {
      if(length(scale) == 2) tlab <- scale[2]
      else tlab <- paste("exp(", scale[1], ")", sep="")
    }
    else tlab <- scale[1]
  }

  if(!length(scale)) tlab <- ''  ## mainly for Glm fits
  if(!missing(main)) tlab <- main
  
  fmt <- function(k) {
    m <- length(k)
    f <- character(m)
    for(i in 1 : m) f[i] <- format(k[i])
    f
  }
  sep <- if(isbase) ' - ' else '<br>'
  dif <- x[, 'Diff.']
  ## Reformat for factor predictors
  if(any(is.na(dif))) lab[is.na(dif)] <- sub(' - ', sep, lab[is.na(dif)])
  lb <- ifelse(is.na(x[, 'Diff.']), lab,
               paste(lab, sep,
                     fmt(round(x[, 'High'], declim)), ' : ',
                     fmt(round(x[, 'Low'],  declim)), sep=''))
   
  if(isbase) {
    confbar <-
      function(y, est, se, q, col, col.points,
               pch=17, lwd=rep(3, length(q)), clip=c(-1e30, 1e30),
               fun  = function(x) x, 
               qfun = function(x) ifelse(x==.5, qnorm(x),
                                  ifelse(x < .5, qnorm(x / 2),
                                         qnorm((1 + x) / 2)))) {

        n <- length(q)
        q <- c(1 - rev(q), .5, q)
        a <- fun(est)
        points(a, y, col=col.points, pch=pch)
        a <- fun(est + se * qfun(q))
        a[a < clip[1]] <- NA; a[a > clip[2]] <- NA
        m <- length(q)
        segments(c(a[1], a[m]), y, c(a[2], a[m - 1]), y, col=col[1], lwd=lwd[1])
        if(n > 1) segments(c(a[2], a[m - 1]), y, c(a[3], a[m - 2]),
                           col=col[2], lwd=lwd[2])
        if(n > 2) segments(c(a[3], a[m - 2]), y, c(a[4], a[m - 3]),
                           col=col[3], lwd=lwd[3])
        names(a) <- format(q)
        invisible(a)
      }
    
    augment <- if(log | any(Type == 2)) c(.1, .5, .75, 1) else 0
    n     <- length(effect)
    out   <- qnorm((max(q) + 1) / 2)
    if(missing(xlim) && !missing(at))
      xlim <- range(if(log) logb(at) else at)
    else
      if(missing(xlim)) {
        xlim <- fun(range(c(effect - out * se, effect + out * se)))
        xlim[1] <- max(xlim[1], clip[1])
        xlim[2] <- min(xlim[2], clip[2])
      }
    else
      augment <- c(augment, if(log) exp(xlim) else xlim)
    
    plot.new(); par(new=TRUE)
    mxlb <- .1 + max(strwidth(lb, units='inches', cex=cex))
    tmai <- par('mai')
    on.exit(par(mai=tmai))
    par(mai=c(tmai[1], mxlb, 1.5*tmai[3], tmai[4]))
    
    outer.widths <- fun(effect + out * se) - fun(effect - out * se)
    if(missing(nbar)) nbar <- n
    npage <- ceiling(n/nbar)
    is <- 1
    for(p in 1 : npage) {
      ie <- min(is + nbar - 1, n)
      plot(1:nbar, rep(0,nbar), xlim=xlim, ylim=c(1,nbar),
           type="n", axes=FALSE, 
           xlab="", ylab="")
      if(cex.main > 0) title(tlab, cex=cex.main)
      lines(fun(c(0, 0)), c(nbar - (ie - is), nbar), lty=2)
      if(log) {
        pxlim <- pretty(exp(xlim), n=nint)
        pxlim <- sort(unique(c(pxlim, augment)))
        ## For wome weird reason, sometimes duplicates (at xlim[2])
        ## still remain
        pxlim <- pxlim[pxlim >= exp(xlim[1])]
        if(!missing(at)) pxlim <- at
        axis(3, logb(pxlim), labels=format(pxlim))
      }
      else {
        pxlim <- pretty(xlim, n=nint)
        pxlim <- sort(unique(c(pxlim, augment)))
        pxlim <- pxlim[pxlim >= xlim[1]]
        if(!missing(at)) pxlim <- at
        axis(3, pxlim)
      }
      imax <- (is : ie)[outer.widths[is : ie] == max(outer.widths[is : ie])][1]
      for(i in is : ie) {
        confbar(nbar - (i - is + 1) + 1, effect[i], se[i], q=q,
                col=col, col.points=col.points,
                fun=fun, clip=clip, lwd=lwd, pch=pch)
        mtext(lb[i], 2, 0, at=nbar - (i - is + 1) + 1, cex=cex,
              adj=1, las=1)
      }
      if(adjust != "") {
        xx <- par('usr')[2]
        if(nbar > ie) text(xx, nbar - (ie - is + 1), adjust, adj=1, cex=cex)
        else title(sub=adjust, adj=1, cex=cex)
      }
      is <- ie + 1
    }
    return(invisible())
  }
  
  ## Use plotly instead
  
  qfun <- function(x) ifelse(x == 0.5, qnorm(x),
                      ifelse(x  < 0.5, qnorm(x / 2),
                                       qnorm((1 + x) / 2)))

  ## ??? don't we need a different qfun for ols using t dist?
  
  n       <- length(q)
  feffect <- fun(effect)
  hte     <- format(feffect, digits=digits)
  if(adjust != '') hte <- paste(hte, adjust, sep='<br>')

  height <- pp$heightDotchartb(lb)
  auto   <- .Options$plotlyauto
  if(length(auto) && auto) height <- NULL
  
  p <- plotly::plot_ly(x=~ feffect, y=~ lb,
                       text = ~ hte,
                       type = 'scatter', mode='markers', hoverinfo='text',
                       name = 'Estimate',
                       height = height)

  
  for(i in 1 : n) {
    lower <- fun(effect + se * qfun(1. - q[i]))
    upper <- fun(effect + se * qfun(q[i]))
    ## Interrupt line segments with NA
    m <- 2 * length(effect)
    x <- rep(NA, m)
    x[seq(1, m, by=2)] <- lower
    x[seq(2, m, by=2)] <- upper
    ycl <- rep(lb, each=2)
    ht <-ifelse(is.na(x), '', format(x, digits=digits))
    cl95 <- which(abs(q - 0.95) < 0.000001)
    vis  <- ! length(cl95) || i %in% cl95
    dat <- data.frame(x, ycl, ht)
    p <- plotly::add_markers(p, x=~ x, y=~ ycl, text=~ ht, data=dat,
                             marker = list(symbol='line-ns-open'),
                             hoverinfo = 'text',
                             name = paste(format(q)[i], 'CI'),
                             visible = if(vis) TRUE else 'legendonly')
  }
  
  plotly::layout(p,
                 xaxis = list(type = if(log) 'log' else 'linear',
                              zeroline=FALSE, title=tlab),
                 yaxis  = list(title='', autorange='reversed'),
                 margin = list(l=pp$lrmargin(lb)),
                 shapes = list(
                   list(type = "line",
                        line = list(color = "lightgray"), 
                        x0 =fun(0), x1 = fun(0), xref = "x",
                        y0 = 0, y1=length(lb), yref='y'))
                 )
}

Try the rms package in your browser

Any scripts or data that you put into this service are public.

rms documentation built on Sept. 12, 2023, 9:07 a.m.