From d6f579b3cd9b242c608ed19b055d023662f7edc7 Mon Sep 17 00:00:00 2001
From: "Pavel N. Krivitsky"
Date: Mon, 18 Dec 2023 18:22:03 +1100
Subject: [PATCH] Implemented ginv_eigen(), an alternative to MASS:ginv() that
uses eigendecomposition instead of QR decompsition and its scaled and xTAx
variants. Bumped minor version to 4.10.
---
DESCRIPTION | 4 +--
NAMESPACE | 3 ++
R/matrix.utils.R | 76 ++++++++++++++++++++++++++++++++++++++++--------
man/ssolve.Rd | 29 +++++++++++++-----
man/xTAx.Rd | 16 ++++++++++
5 files changed, 106 insertions(+), 22 deletions(-)
diff --git a/DESCRIPTION b/DESCRIPTION
index 8b9da83..8c5c2c4 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
Package: statnet.common
-Version: 4.9.0-436
-Date: 2023-06-27
+Version: 4.10.0-438
+Date: 2023-12-18
Title: Common R Scripts and Utilities Used by the Statnet Project Software
Authors@R: c(
person(c("Pavel", "N."), "Krivitsky", role=c("aut","cre"), email="pavel@statnet.org", comment=c(ORCID="0000-0002-9101-3362", affiliation="University of New South Wales")),
diff --git a/NAMESPACE b/NAMESPACE
index ce89c57..ffb7833 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -86,6 +86,7 @@ export(eval_lhs.formula)
export(filter_rhs.formula)
export(fixed.pval)
export(forkTimeout)
+export(ginv_eigen)
export(handle.controls)
export(is.SPD)
export(is.linwmatrix)
@@ -140,8 +141,10 @@ export(update_snctrl)
export(vector.namesmatch)
export(xAxT)
export(xTAx)
+export(xTAx_eigen)
export(xTAx_qrsolve)
export(xTAx_qrssolve)
+export(xTAx_seigen)
export(xTAx_solve)
export(xTAx_ssolve)
importFrom(coda,as.mcmc)
diff --git a/R/matrix.utils.R b/R/matrix.utils.R
index f02ae27..8afbfcf 100644
--- a/R/matrix.utils.R
+++ b/R/matrix.utils.R
@@ -85,6 +85,27 @@ sandwich_solve <- function(A, B, ...) {
solve(A, t(solve(A, B, ...)), ...)
}
+#' @describeIn xTAx Evaluate \eqn{x' A^{-1} x} for vector \eqn{x} and
+#' matrix \eqn{A} (symmetric, nonnegative-definite) via
+#' eigendecomposition; returns `rank` and `nullity` as attributes
+#' just in case subsequent calculations (e.g., hypothesis test
+#' degrees of freedom) are affected.
+#'
+#' Decompose \eqn{A = P L P'} for \eqn{L} diagonal matrix of
+#' eigenvalues and \eqn{P} orthogonal. Then \eqn{A^{-1} = P L^{-1}
+#' P'}.
+#'
+#' Substituting, \deqn{x' A^{-1} x = x' P L^{-1} P' x
+#' = h' L^{-1} h} for \eqn{h = P' x}.
+#'
+#' @export
+xTAx_eigen <- function(x, A, tol=sqrt(.Machine$double.eps), ...) {
+ e <- eigen(A, symmetric=TRUE)
+ keep <- e$values > max(tol * e$values[1L], 0)
+ h <- drop(crossprod(x, e$vectors[, keep, drop=FALSE]))
+ structure(sum(h*h/e$values[keep]), rank = sum(keep), nullity = sum(!keep))
+}
+
#' Wrappers around matrix algebra functions that pre-*s*cale their
#' arguments
@@ -96,16 +117,23 @@ sandwich_solve <- function(A, B, ...) {
#' functions first scale the matrix's rows and/or columns by its
#' diagonal elements and then undo the scaling on the result.
#'
-#' `ssolve()`, `sginv()`, and `snearPD()` wrap [solve()],
-#' [MASS::ginv()], and [Matrix::nearPD()], respectively. `srcond()`
-#' returns the reciprocal condition number of [rcond()] net of the
-#' above scaling. `xTAx_ssolve`, `xTAx_qrssolve`, and
-#' `sandwich_ssolve` wrap the corresponding \pkg{statnet.common}
-#' functions.
+#' `ginv_eigen()` reimplements [MASS::ginv()] but using
+#' eigendecomposition rather than SVD; this means that it is only
+#' suitable for symmetric matrices, but that detection of negative
+#' eigenvalues is more robust.
+#'
+#' `ssolve()`, `sginv()`, `sginv_eigen()`, and `snearPD()` wrap
+#' [solve()], [MASS::ginv()], `ginv_eigen()`, and [Matrix::nearPD()],
+#' respectively. `srcond()` returns the reciprocal condition number of
+#' [rcond()] net of the above scaling. `xTAx_ssolve()`,
+#' `xTAx_qrssolve()`, `xTAx_seigen()`, and `sandwich_ssolve()` wrap
+#' the corresponding \pkg{statnet.common} functions.
#'
#' @param snnd assume that the matrix is symmetric non-negative
-#' definite (SNND). If it's "obvious" that it's not (e.g., negative
-#' diagonal elements), an error is raised.
+#' definite (SNND). This typically entails scaling that converts
+#' covariance to correlation and use of eigendecomposition rather
+#' than singular-value decomposition. If it's "obvious" that the matrix is
+#' not SSND (e.g., negative diagonal elements), an error is raised.
#'
#' @param x,a,b,X,A,B,tol,... corresponding arguments of the wrapped functions.
#'
@@ -135,7 +163,7 @@ ssolve <- function(a, b, ..., snnd = TRUE) {
#' @rdname ssolve
#'
#' @export
-sginv <- function(X, ..., snnd = TRUE) {
+sginv <- function(X, ..., snnd = TRUE){
d <- diag(as.matrix(X))
d <- ifelse(d==0, 1, 1/d)
@@ -143,14 +171,38 @@ sginv <- function(X, ..., snnd = TRUE) {
d <- sqrt(d)
if(anyNA(d)) stop("Matrix a has negative elements on the diagonal, and snnd=TRUE.")
dd <- rep(d, each = length(d)) * d
- X <- X * dd
- MASS::ginv(X, ...) * dd
+ ginv_eigen(X * dd, ...) * dd
} else {
dd <- rep(d, each = length(d))
- MASS::ginv(X*d, ...) * dd
+ MASS::ginv(X * dd, ...) * t(dd)
}
}
+#' @rdname ssolve
+#' @export
+ginv_eigen <- function(X, tol = sqrt(.Machine$double.eps), ...){
+ e <- eigen(X, symmetric=TRUE)
+ keep <- e$values > max(tol * e$values[1L], 0)
+ tcrossprod(e$vectors[, keep, drop=FALSE] / rep(e$values[keep],each=ncol(X)), e$vectors[, keep, drop=FALSE])
+}
+
+
+#' @rdname ssolve
+#'
+#' @export
+xTAx_seigen <- function(x, A, tol=sqrt(.Machine$double.eps), ...) {
+ d <- diag(as.matrix(A))
+ d <- ifelse(d<=0, 0, 1/d)
+
+ d <- sqrt(d)
+ dd <- rep(d, each = length(d)) * d
+
+ A <- A * dd
+ x <- x * d
+
+ xTAx_eigen(x, A, tol=tol, ...)
+}
+
#' @rdname ssolve
#'
#' @export
diff --git a/man/ssolve.Rd b/man/ssolve.Rd
index 2bb478b..b14142f 100644
--- a/man/ssolve.Rd
+++ b/man/ssolve.Rd
@@ -3,6 +3,8 @@
\name{ssolve}
\alias{ssolve}
\alias{sginv}
+\alias{ginv_eigen}
+\alias{xTAx_seigen}
\alias{srcond}
\alias{snearPD}
\alias{xTAx_ssolve}
@@ -15,6 +17,10 @@ ssolve(a, b, ..., snnd = TRUE)
sginv(X, ..., snnd = TRUE)
+ginv_eigen(X, tol = sqrt(.Machine$double.eps), ...)
+
+xTAx_seigen(x, A, tol = sqrt(.Machine$double.eps), ...)
+
srcond(x, ..., snnd = TRUE)
snearPD(x, ...)
@@ -27,8 +33,10 @@ sandwich_ssolve(A, B, ...)
}
\arguments{
\item{snnd}{assume that the matrix is symmetric non-negative
-definite (SNND). If it's "obvious" that it's not (e.g., negative
-diagonal elements), an error is raised.}
+definite (SNND). This typically entails scaling that converts
+covariance to correlation and use of eigendecomposition rather
+than singular-value decomposition. If it's "obvious" that the matrix is
+not SSND (e.g., negative diagonal elements), an error is raised.}
\item{x, a, b, X, A, B, tol, ...}{corresponding arguments of the wrapped functions.}
}
@@ -41,12 +49,17 @@ functions first scale the matrix's rows and/or columns by its
diagonal elements and then undo the scaling on the result.
}
\details{
-\code{ssolve()}, \code{sginv()}, and \code{snearPD()} wrap \code{\link[=solve]{solve()}},
-\code{\link[MASS:ginv]{MASS::ginv()}}, and \code{\link[Matrix:nearPD]{Matrix::nearPD()}}, respectively. \code{srcond()}
-returns the reciprocal condition number of \code{\link[=rcond]{rcond()}} net of the
-above scaling. \code{xTAx_ssolve}, \code{xTAx_qrssolve}, and
-\code{sandwich_ssolve} wrap the corresponding \pkg{statnet.common}
-functions.
+\code{ginv_eigen()} reimplements \code{\link[MASS:ginv]{MASS::ginv()}} but using
+eigendecomposition rather than SVD; this means that it is only
+suitable for symmetric matrices, but that detection of negative
+eigenvalues is more robust.
+
+\code{ssolve()}, \code{sginv()}, \code{sginv_eigen()}, and \code{snearPD()} wrap
+\code{\link[=solve]{solve()}}, \code{\link[MASS:ginv]{MASS::ginv()}}, \code{ginv_eigen()}, and \code{\link[Matrix:nearPD]{Matrix::nearPD()}},
+respectively. \code{srcond()} returns the reciprocal condition number of
+\code{\link[=rcond]{rcond()}} net of the above scaling. \code{xTAx_ssolve()},
+\code{xTAx_qrssolve()}, \code{xTAx_seigen()}, and \code{sandwich_ssolve()} wrap
+the corresponding \pkg{statnet.common} functions.
}
\examples{
x <- rnorm(2, sd=c(1,1e12))
diff --git a/man/xTAx.Rd b/man/xTAx.Rd
index 4bb067e..ceffe12 100644
--- a/man/xTAx.Rd
+++ b/man/xTAx.Rd
@@ -6,6 +6,7 @@
\alias{xTAx_solve}
\alias{xTAx_qrsolve}
\alias{sandwich_solve}
+\alias{xTAx_eigen}
\title{Common quadratic forms}
\usage{
xTAx(x, A)
@@ -17,6 +18,8 @@ xTAx_solve(x, A, ...)
xTAx_qrsolve(x, A, tol = 1e-07, ...)
sandwich_solve(A, B, ...)
+
+xTAx_eigen(x, A, tol = sqrt(.Machine$double.eps), ...)
}
\arguments{
\item{x}{a vector}
@@ -56,4 +59,17 @@ and \code{nullity} as attributes just in case subsequent calculations
\item \code{sandwich_solve()}: Evaluate \eqn{A^{-1}B(A')^{-1}} for \eqn{B} a
square matrix and \eqn{A} invertible.
+\item \code{xTAx_eigen()}: Evaluate \eqn{x' A^{-1} x} for vector \eqn{x} and
+matrix \eqn{A} (symmetric, nonnegative-definite) via
+eigendecomposition; returns \code{rank} and \code{nullity} as attributes
+just in case subsequent calculations (e.g., hypothesis test
+degrees of freedom) are affected.
+
+Decompose \eqn{A = P L P'} for \eqn{L} diagonal matrix of
+eigenvalues and \eqn{P} orthogonal. Then \eqn{A^{-1} = P L^{-1}
+ P'}.
+
+Substituting, \deqn{x' A^{-1} x = x' P L^{-1} P' x
+ = h' L^{-1} h} for \eqn{h = P' x}.
+
}}