Nothing
## Weighted-L2 rational coefficients: the fit itself, the coefficient tables,
## the caching, and the models built from them with matern.operators(),
## matern.rational() and rspde_lme(). The tests of the INLA interface are in
## test.rational.wl2.inla.R, which needs the compiled cgeneric library.
test_that("weighted-L2 coefficients reproduce the reference values", {
## Reference values from the numpy/scipy implementation the R code was
## ported from. The tolerance is 2% because the quadrature grids differ
## slightly; the largest pole is checked to within 10%.
cases <- list(
list(args = list(alpha = 0.75, d = 1, m = 2), err = 1.4265e-2, b = 5.187e1),
list(args = list(alpha = 0.75, d = 1, m = 4), err = 1.3442e-3, b = 7.925e3),
list(args = list(alpha = 1.5, d = 2, m = 2), err = 2.2597e-3, b = 8.525e1),
list(args = list(alpha = 1.5, d = 2, m = 4), err = 8.5549e-5, b = 2.645e3),
list(
args = list(alpha = 0.75, d = 1, m = 4, x_min = 1 / 401),
err = 9.9513e-5, b = 4.524e2
),
list(
args = list(alpha = 1.5, d = 2, m = 4, x_min = 1 / 801),
err = 3.2750e-5, b = 1.098e3
),
list(
args = list(alpha = 1.25, d = 1, m = 3, x_min = 1e-4),
err = 2.0336e-4, b = 1.593e3
),
list(
args = list(
alpha = 1.5, d = 2, m = 2, x_min = 1 / 801, type = "operator"
),
err = 2.7957e-3, b = 3.819e2
),
list(
args = list(
alpha = 1.5, d = 2, m = 4, x_min = 1 / 801, type = "operator"
),
err = 4.3432e-5, b = 1.580e3
),
## floor(alpha) = 2, from the same reference implementation
## (handoff/wl2_reference.py of the Rational project, kind "shifted2")
list(args = list(alpha = 2.4, d = 1, m = 2), err = 7.3610e-5, b = 3.719e1),
list(args = list(alpha = 2.4, d = 1, m = 4), err = 5.5299e-7, b = 4.044e2),
list(args = list(alpha = 2.8, d = 2, m = 2), err = 5.8425e-5, b = 1.338e1),
list(args = list(alpha = 2.8, d = 2, m = 4), err = 3.7251e-7, b = 1.385e2),
list(
args = list(alpha = 2.4, d = 2, m = 3, x_min = 1 / 801),
err = 1.7144e-5, b = 1.820e2
)
)
for (case in cases) {
cf <- do.call(rational.coefficients.wl2, case$args)
expect_equal(cf$rel_err, case$err, tolerance = 0.02)
expect_equal(max(1 - cf$p), case$b, tolerance = 0.1)
}
})
test_that("weighted-L2 coefficients are in the feasible set", {
for (alpha in c(0.6, 0.95, 1.1, 1.75, 2.05, 2.6, 2.95)) {
for (m in 1:4) {
cf <- rational.coefficients.wl2(alpha, d = 1, m = m)
expect_true(all(cf$r >= 0))
expect_true(all(cf$p < 1))
expect_equal(cf$k, 0)
expect_equal(length(cf$r), m)
if (floor(alpha) == 0) {
expect_null(cf$p0)
} else {
expect_true(cf$p0 >= 0 && cf$p0 < 1)
}
}
}
expect_error(rational.coefficients.wl2(3.5, d = 1, m = 2), "0, 1 or 2")
expect_error(
rational.coefficients.wl2(2.5, d = 1, m = 2, type = "operator"),
"smaller than 2"
)
})
test_that("the symbol of the weighted-L2 fit approximates x^alpha", {
cf <- rational.coefficients.wl2(1.5, d = 2, m = 4, x_min = 1e-4)
x <- exp(seq(log(1e-4), 0, length.out = 200))
expect_equal(rSPDE:::wl2_symbol(cf, x), x^1.5, tolerance = 1e-2)
})
test_that("the coefficient table interpolates as well as a direct fit", {
for (m_alpha in 1:2) {
tab <- rSPDE:::wl2_coefficient_table(d = 1, m = 4, m_alpha = m_alpha)
expect_equal(rSPDE:::wl2_q(attr(tab, "kind")), m_alpha)
for (alpha in m_alpha + c(0.113, 0.457, 0.802)) {
ci <- rSPDE:::wl2_interp_coefficients(tab, alpha)
direct <- rational.coefficients.wl2(alpha, d = 1, m = 4)
expect_equal(ci$rel_err, direct$rel_err, tolerance = 0.05)
expect_equal(ci$q, m_alpha)
expect_true(all(ci$r > 0))
expect_true(all(ci$p < 1))
}
}
})
test_that("the weighted-L2 blocks reproduce the rational symbol on a mesh", {
## Each block must be symmetric positive definite, and the covariance of the
## approximation must equal V diag(r(x_j)) V^T from the generalised
## eigendecomposition of (G, C).
x <- seq(from = 0, to = 1, length.out = 101)
n <- length(x)
fem <- rSPDE.fem1d(x)
Cl <- Matrix::Diagonal(n, rowSums(fem$C))
kappa <- 15
sigma <- 1
d <- 1
for (nu in c(0.25, 1, 1.9)) {
alpha <- nu + d / 2
m <- 2
op <- matern.operators(
loc_mesh = x, nu = nu, range = sqrt(8 * nu) / kappa, sigma = sigma,
d = d, m = m, parameterization = "matern",
type_rational_approximation = "wl2"
)
## m blocks, not m + 1
expect_equal(dim(op$Q)[1], m * n)
expect_true(Matrix::isSymmetric(op$Q, tol = 1e-9))
for (j in seq_len(m)) {
idx <- (j - 1) * n + seq_len(n)
Qj <- op$Q[idx, idx, drop = FALSE]
expect_true(Matrix::isSymmetric(Qj, tol = 1e-9))
expect_true(min(eigen(as.matrix(Qj), only.values = TRUE)$values) > 0)
}
L <- (fem$G + kappa^2 * Cl) / kappa^2
ev <- eigen(as.matrix(Matrix::solve(Cl, L)))
lambda <- Re(ev$values)
V <- Re(ev$vectors)
V <- sweep(V, 2, sqrt(diag(t(V) %*% as.matrix(Cl) %*% V)), "/")
tau <- sqrt(gamma(nu) / (sigma^2 * kappa^(2 * nu) *
(4 * pi)^(d / 2) * gamma(nu + d / 2)))
cf <- rSPDE:::wl2_interp_coefficients(op$wl2_table, alpha)
symbol <- rSPDE:::wl2_symbol(cf, 1 / lambda)
Sigma_ref <- V %*% diag(symbol) %*% t(V) / (tau^2 * kappa^(2 * alpha))
A <- kronecker(matrix(1, 1, m), Matrix::Diagonal(n))
Sigma <- as.matrix(A %*% Matrix::solve(op$Q, t(A)))
expect_equal(max(abs(Sigma - Sigma_ref)), 0, tolerance = 1e-8)
}
})
test_that("the weighted-L2 blocks have the same sparsity as the tabulated ones", {
x <- seq(from = 0, to = 1, length.out = 51)
n <- length(x)
for (nu in c(0.25, 1)) {
op_wl2 <- matern.operators(
loc_mesh = x, nu = nu, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2"
)
op_tab <- matern.operators(
loc_mesh = x, nu = nu, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "brasil"
)
nnz <- function(Q, j, n) {
idx <- (j - 1) * n + seq_len(n)
sum(Q[idx, idx, drop = FALSE] != 0)
}
expect_equal(nnz(op_wl2$Q, 1, n), nnz(op_tab$Q, 1, n))
}
})
test_that("the weighted-L2 covariance beats the tabulated one", {
## d = 1, m = 4, kappa * h = 0.1, compared with the exact covariance of the
## discretised model, for one nu in each of the three classes.
d <- 1
m <- 4
sigma <- 1
kappa <- 20
h <- 0.1 / kappa
x <- seq(from = 0, to = 1, by = h)
n <- length(x)
fem <- rSPDE.fem1d(x)
Cl <- Matrix::Diagonal(n, rowSums(fem$C))
L <- (fem$G + kappa^2 * Cl) / kappa^2
ev <- eigen(as.matrix(Matrix::solve(Cl, L)))
lambda <- Re(ev$values)
V <- Re(ev$vectors)
V <- sweep(V, 2, sqrt(diag(t(V) %*% as.matrix(Cl) %*% V)), "/")
for (nu in c(0.25, 0.9, 1.9)) {
alpha <- nu + d / 2
tau <- sqrt(gamma(nu) / (sigma^2 * kappa^(2 * nu) *
(4 * pi)^(d / 2) * gamma(nu + d / 2)))
Sigma_exact <- V %*% diag(lambda^(-alpha)) %*% t(V) /
(tau^2 * kappa^(2 * alpha))
err <- sapply(c("wl2", "brasil"), function(ty) {
op <- matern.operators(
loc_mesh = x, nu = nu, range = sqrt(8 * nu) / kappa, sigma = sigma,
d = d, m = m, parameterization = "matern",
type_rational_approximation = ty
)
A <- kronecker(
matrix(1, 1, rSPDE:::rspde_n_blocks_obj(op)), Matrix::Diagonal(n)
)
Sigma <- as.matrix(A %*% Matrix::solve(op$Q, t(A)))
max(abs(Sigma - Sigma_exact)) / sigma^2
})
expect_lt(err[["wl2"]], err[["brasil"]])
}
})
test_that("the nodal variance correction removes the variance deficit", {
## The correction fills a deficit and never subtracts: away from the boundary
## the nodal variance becomes sigma^2 exactly, while within about one range
## of the boundary the model already has more variance than sigma^2 and is
## left alone (the exact covariance of the Neumann problem is the folded
## Matern, whose variance doubles there).
x <- seq(from = 0, to = 4, length.out = 401)
mk <- function(vc) {
matern.operators(
loc_mesh = x, nu = 0.3, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2",
variance_correction = vc
)
}
op <- mk("nodal")
expect_equal(op$variance_correction, "nodal")
expect_true(all(op$nodal_correction >= 0))
inner <- which(x > 1 & x < 3)
v0 <- as.vector(Matrix::diag(covariance_mesh(mk("none"))))
v1 <- as.vector(Matrix::diag(covariance_mesh(op)))
## there is a deficit to remove, and it is removed in the interior
expect_gt(max(1 - v0[inner]), 1e-3)
expect_equal(v1[inner], rep(1, length(inner)), tolerance = 1e-8)
## and nothing is taken away anywhere
expect_true(all(v1 >= v0 - 1e-12))
})
test_that("the variance correction is off by default and only where it means something", {
x <- seq(from = 0, to = 4, length.out = 201)
mk <- function(...) {
matern.operators(
loc_mesh = x, nu = 0.3, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "covariance", ...
)
}
## off unless asked for, and then the default model is exactly the
## uncorrected one
default <- mk(type_rational_approximation = "wl2")
expect_equal(default$variance_correction, "none")
expect_null(default$nodal_correction)
expect_equal(
as.matrix(covariance_mesh(default)),
as.matrix(covariance_mesh(mk(
type_rational_approximation = "wl2", variance_correction = "none"
)))
)
## and it survives an update in either state
expect_equal(update(default, nu = 0.35)$variance_correction, "none")
on <- mk(type_rational_approximation = "wl2", variance_correction = "nodal")
expect_equal(update(on, nu = 0.35)$variance_correction, "nodal")
## It is specific to the covariance-based weighted-L2 construction: the
## tabulated classes have a constant term already, and the operator-based
## construction forms the covariance as r(x)^2 from factors, which a
## diagonal addition does not fit. Both combinations warn rather than
## quietly doing nothing.
expect_warning(
mk(type_rational_approximation = "brasil", variance_correction = "nodal"),
"only used with type_rational_approximation = 'wl2'"
)
expect_warning(
matern.operators(
loc_mesh = x, nu = 0.3, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "operator",
type_rational_approximation = "wl2", variance_correction = "nodal"
),
"only used with type = 'covariance'"
)
})
test_that("the corrected covariance is still a covariance", {
## Targeting sigma^2 at the boundary, where the model correctly has about
## 2 sigma^2, used to subtract about 1 from the diagonal there and took the
## smallest eigenvalue from about +0.05 to about -0.9.
x <- seq(from = 0, to = 4, length.out = 201)
for (nu in c(0.3, 0.6, 1.2)) {
for (m in c(1, 2, 4)) {
mineig <- function(vc) {
S <- as.matrix(covariance_mesh(matern.operators(
loc_mesh = x, nu = nu, range = 0.2, sigma = 1, d = 1, m = m,
parameterization = "matern", type_rational_approximation = "wl2",
variance_correction = vc
)))
min(eigen((S + t(S)) / 2, symmetric = TRUE, only.values = TRUE)$values)
}
expect_gt(mineig("nodal"), 0)
## the correction only adds a non-negative diagonal, so it cannot make
## the smallest eigenvalue smaller
expect_gte(mineig("nodal"), mineig("none") - 1e-10)
}
}
})
test_that("the nodal correction improves the covariance where there is a deficit", {
## Against the folded Matern on the interior of a domain long relative to the
## range, so that sigma^2 is the right nodal variance there. The gain is
## largest at low smoothness, where a mesh represents a rough field worst:
## the deficit is mostly finite element discretisation error, not rational
## approximation error, so it does not shrink with m.
skip_on_cran()
L <- 4
n <- 401
x <- seq(0, L, length.out = n)
h <- x[2] - x[1]
cl <- rep(h, n)
cl[c(1, n)] <- h / 2
inner <- which(x > 1 & x < L - 1)
nu <- 0.3
rho <- 0.2
kappa <- sqrt(8 * nu) / rho
target <- matrix(0, n, n)
for (k in -8:8) {
for (sg in c(1, -1)) {
target <- target + matern.covariance(
abs(outer(x, sg * x + 2 * L * k, "-")),
kappa = kappa, nu = nu, sigma = 1
)
}
}
ci <- sqrt(cl[inner])
err <- function(vc, m) {
S <- as.matrix(covariance_mesh(matern.operators(
loc_mesh = x, nu = nu, range = rho, sigma = 1, d = 1, m = m,
parameterization = "matern", type_rational_approximation = "wl2",
variance_correction = vc
)))
E <- S[inner, inner] - target[inner, inner]
c(L2 = sqrt(sum((ci * t(ci * E))^2)), sup = max(abs(E)))
}
for (m in c(2, 4)) {
a <- err("none", m)
b <- err("nodal", m)
## measured: L2 3.5x (m = 2) and 10x (m = 4), sup 13.5x and 18.6x
expect_gt(a[["L2"]] / b[["L2"]], 2.5)
expect_gt(a[["sup"]] / b[["sup"]], 8)
}
})
test_that("the operator-based conversion reproduces the rational symbol", {
for (args in list(
list(alpha = 1.5, d = 2, m = 2), list(alpha = 1.5, d = 2, m = 4),
list(alpha = 0.75, d = 1, m = 2), list(alpha = 1.9, d = 1, m = 3)
)) {
cf <- do.call(rational.coefficients.wl2, c(args, list(
x_min = 1 / 801, type = "operator"
)))
roots <- rSPDE:::wl2_roots(cf)
expect_equal(length(roots$rb), args$m + 1)
expect_equal(length(roots$rc), args$m)
expect_true(all(roots$rb < 0))
expect_true(all(roots$rc < 0))
## the numerator roots interlace the poles
expect_true(all(diff(sort(c(1 / roots$rb, 1 / roots$rc))) != 0))
lambda <- exp(seq(0, log(801), length.out = 100))
symbol <- roots$factor *
apply(outer(lambda, roots$rc, function(l, r) 1 - r * l), 1, prod) /
apply(outer(lambda, roots$rb, function(l, r) 1 - r * l), 1, prod)
expect_equal(symbol, lambda^(-args$alpha / 2), tolerance = 5e-2)
}
})
test_that("operator-based models can be built with the weighted-L2 type", {
x <- seq(from = 0, to = 1, length.out = 101)
op <- matern.operators(
loc_mesh = x, nu = 0.4, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "operator",
type_rational_approximation = "wl2"
)
expect_equal(op$type_rational_approximation, "wl2")
expect_true(op$x_min > 0 && op$x_min < 1)
expect_equal(length(simulate(op)), length(x))
## a user-supplied lower bound for kappa tightens the interval
op2 <- matern.operators(
loc_mesh = x, nu = 0.4, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "operator",
type_rational_approximation = "wl2", kappa_ref = sqrt(8 * 0.4) / 0.6
)
expect_true(op2$x_min > op$x_min)
## alpha >= 2 is out of scope for the operator type
expect_error(matern.operators(
loc_mesh = x, nu = 1.8, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "operator",
type_rational_approximation = "wl2"
))
})
test_that("covariance-based methods work with the weighted-L2 type", {
set.seed(123)
x <- seq(from = 0, to = 1, length.out = 101)
op <- matern.operators(
loc_mesh = x, nu = 0.8, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2",
compute_logdet = TRUE
)
A <- Matrix::Diagonal(length(x))
Y <- as.vector(simulate(op)) + rnorm(length(x), sd = 0.1)
expect_equal(length(simulate(op)), length(x))
ll <- rSPDE.matern.loglike(op, Y, A = A, sigma.e = 0.1)
expect_true(is.finite(ll))
## the log-determinant shortcut must agree with the direct computation
op2 <- update(op, compute_higher_order = FALSE)
op2$compute_logdet <- FALSE
expect_equal(ll, rSPDE.matern.loglike(op2, Y, A = A, sigma.e = 0.1),
tolerance = 1e-8
)
pred <- predict(op, A = A, Aprd = A, Y = Y, sigma.e = 0.1,
compute.variances = TRUE
)
expect_equal(length(pred$mean), length(x))
expect_true(all(pred$variance > 0))
expect_equal(dim(covariance_mesh(op)), c(length(x), length(x)))
expect_equal(
dim(cov_function_mesh(op, p = matrix(0.5, 1, 1))), c(length(x), 1)
)
## floor(alpha) >= 3 is out of scope
expect_error(matern.operators(
loc_mesh = x, nu = 2.8, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2"
), "0, 1 or 2")
})
test_that("rspde.xmin is a lower bound for the spectral interval", {
x <- seq(from = 0, to = 1, length.out = 401)
fem <- rSPDE.fem1d(x)
Cl <- Matrix::Diagonal(length(x), rowSums(fem$C))
kappa <- 10
x_min <- rspde.xmin(C = Cl, G = fem$G, kappa_ref = kappa)
L <- (fem$G + kappa^2 * Cl) / kappa^2
lambda_max <- max(Re(eigen(as.matrix(Matrix::solve(Cl, L)),
only.values = TRUE
)$values))
expect_lte(x_min, 1 / lambda_max)
## a smaller reference kappa gives a wider interval
expect_lt(rspde.xmin(C = Cl, G = fem$G, kappa_ref = 1), x_min)
})
test_that("the weighted-L2 type is rejected where it is not implemented", {
expect_error(rSPDE:::get_rational_coefficients(2, "wl2"), "computed at")
})
test_that("updating a model keeps the weighted-L2 settings", {
x <- seq(from = 0, to = 1, length.out = 51)
for (type in c("covariance", "operator")) {
op <- matern.operators(
loc_mesh = x, nu = 0.4, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = type,
type_rational_approximation = "wl2"
)
op2 <- update(op, range = 0.1)
expect_equal(op2$type_rational_approximation, "wl2")
expect_equal(op2$x_min, op$x_min)
## the two-stage helper tightens the interval
op3 <- update_rational_coefficients(op)
expect_equal(op3$type_rational_approximation, "wl2")
expect_true(op3$x_min > (if (is.null(op$x_min)) 0 else op$x_min))
}
op_tab <- matern.operators(
loc_mesh = x, nu = 0.4, range = 0.2, sigma = 1, d = 1, m = 2,
parameterization = "matern", type = "operator"
)
expect_error(update_rational_coefficients(op_tab), "wl2")
})
test_that("rspde_lme works with the weighted-L2 type", {
skip_on_cran()
set.seed(42)
x <- seq(from = 0, to = 1, length.out = 101)
op_true <- matern.operators(
loc_mesh = x, nu = 0.8, range = 0.3, sigma = 1, d = 1, m = 2,
parameterization = "matern"
)
u <- as.vector(simulate(op_true))
loc <- runif(80)
Y <- as.vector(rSPDE.A1d(x, loc) %*% u) + rnorm(80, sd = 0.1)
df <- data.frame(y = Y, x = loc)
op <- matern.operators(
loc_mesh = x, nu = 0.8, range = 0.3, sigma = 1, d = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2"
)
## the smoothness bound is reduced so that alpha stays below 3
expect_message(
fit <- rspde_lme(y ~ 1, loc = "x", data = df, model = op, parallel = FALSE),
"alpha < 3"
)
## and nu stays inside what the classes support, floor(alpha) at most 2
expect_lt(fit$coeff$random_effects[["nu"]] + 1 / 2, 3)
expect_true(is.finite(fit$loglik))
pred <- predict(fit, newdata = data.frame(x = c(0.25, 0.75)), loc = "x")
expect_equal(length(as.vector(pred$mean)), 2)
})
test_that("the shifted term equals the tabulated one when p0 = 0", {
## The weighted-L2 term 1/((lambda - p0)(lambda - p)) reduces to the term
## lambda^-1/(lambda - p) of the tabulated classes when p0 = 0, so the
## generalised code must reproduce the existing one exactly.
h <- seq(from = 0, to = 0.6, length.out = 9)
kappa <- 7
for (alpha in c(1.2, 1.7)) {
for (p in c(-0.4, -3.2, -50)) {
for (deriv in 0:2) {
old <- sapply(h, function(hh) {
rSPDE:::matern.p.deriv(hh, 0, kappa, p, alpha, deriv = deriv)
})
new <- sapply(h, function(hh) {
rSPDE:::matern.p.deriv(hh, 0, kappa, p, alpha, deriv = deriv, p0 = 0)
})
expect_equal(new, old, tolerance = 1e-10)
}
}
}
})
test_that("the one-dimensional models work with the weighted-L2 type", {
set.seed(7)
sigma <- 1.3
kappa <- 9
for (nu in c(0.3, 0.9, 1.7)) {
## The joint covariance of the field and its derivatives at two nearly
## coincident locations is singular to working precision once the field
## has two derivatives, for the tabulated types just as much as for "wl2",
## so the smoothest case is taken on a grid rather than on random points.
loc <- if (nu > 1) seq(from = 0, to = 1, length.out = 40) else sort(runif(40))
n <- length(loc)
for (ty in c("brasil", "wl2")) {
tmp <- rSPDE:::matern.rational.ldl(
loc = loc, order = 2, nu = nu, kappa = kappa, sigma = sigma,
type_rational = ty
)
## m blocks for "wl2", m + 1 for the tabulated types; the blocks hold
## the process and its derivative when alpha > 1
## each pole block holds floor(alpha) + 1 entries per location, and the
## k block of the tabulated types holds max(floor(alpha), 1)
fa <- floor(nu + 1 / 2) + 1
expected <- 2 * fa * n
if (ty != "wl2") {
expected <- expected + max(floor(nu + 1 / 2), 1) * n
}
expect_equal(dim(tmp$L)[1], expected)
## the Markov construction must agree with the closed-form covariance
Q <- Matrix::t(tmp$L) %*% tmp$D %*% tmp$L
Sigma <- as.matrix(tmp$A %*% Matrix::solve(Q, Matrix::t(tmp$A)))
Sigma_ref <- matern.rational.cov(
as.matrix(dist(loc)), order = 2, kappa = kappa, nu = nu,
sigma = sigma, type_rational = ty
)
expect_equal(max(abs(Sigma - Sigma_ref)), 0, tolerance = 1e-7)
## the location ordering is a permutation of the field ordering
a <- rSPDE:::matern.rational.precision(
loc = loc, order = 2, nu = nu, kappa = kappa, sigma = sigma,
type_rational = ty, ordering = "field"
)
b <- rSPDE:::matern.rational.precision(
loc = loc, order = 2, nu = nu, kappa = kappa, sigma = sigma,
type_rational = ty, ordering = "location"
)
expect_equal(
max(abs(as.matrix(a$A %*% Matrix::solve(a$Q, Matrix::t(a$A))) -
as.matrix(b$A %*% Matrix::solve(b$Q, Matrix::t(b$A))))),
0,
tolerance = 1e-8
)
}
}
})
test_that("matern.rational supports the weighted-L2 type", {
s <- seq(from = 0, to = 1, length.out = 101)
kappa <- 20
sigma <- 2
for (nu in c(0.3, 0.8, 1.8)) {
err <- sapply(c("wl2", "brasil"), function(ty) {
op <- matern.rational(
loc = s, nu = nu, range = sqrt(8 * nu) / kappa, sigma = sigma, m = 2,
parameterization = "matern", type_rational_approximation = ty
)
expect_equal(op$n_blocks, if (ty == "wl2") 2 else 3)
expect_equal(length(simulate(op)), length(s))
## each pole block holds floor(alpha) + 1 entries per location, and the
## k block of the tabulated types holds max(floor(alpha), 1)
alpha <- nu + 0.5
fa_p <- floor(alpha) + 1
expected <- 2 * fa_p * length(s)
if (ty != "wl2") {
expected <- expected + max(floor(alpha), 1) * length(s)
}
expect_equal(dim(precision(op)$Q)[1], expected)
max(abs(op$covariance(ind = 1) -
matern.covariance(abs(s - s[1]), kappa = kappa, sigma = sigma, nu = nu)))
})
expect_lt(err[["wl2"]], err[["brasil"]])
}
## alpha >= 3 is out of scope
expect_error(matern.rational(
loc = s, nu = 2.8, range = 0.2, sigma = 1, m = 2,
parameterization = "matern", type_rational_approximation = "wl2"
), "floor")
})
test_that("matern.rational.cov evaluates the covariance at the given lags", {
## Regression test: the covariance used to be evaluated at h[1] - h.
s <- seq(from = 0, to = 1, length.out = 51)
kappa <- 10
op <- matern.rational(
loc = s, nu = 0.8, range = sqrt(8 * 0.8) / kappa, sigma = 1, m = 2,
parameterization = "matern"
)
true_cov <- function(ind) {
matern.covariance(abs(s - s[ind]), kappa = kappa, sigma = 1, nu = 0.8)
}
expect_equal(as.vector(op$covariance(ind = 1)), true_cov(1), tolerance = 1e-2)
expect_equal(as.vector(op$covariance(ind = 25)), true_cov(25),
tolerance = 1e-2
)
full <- op$covariance()
expect_equal(dim(full), c(length(s), length(s)))
expect_equal(as.vector(full[, 25]), true_cov(25), tolerance = 1e-2)
})
test_that("the weighted-L2 error decreases with the order", {
## Without continuation in m, a plain multistart settles at the optimum of
## the next smaller order beyond m = 4, with one residue exactly zero.
for (alpha in c(0.8, 1.2, 1.7)) {
errs <- sapply(2:6, function(m) {
cf <- rational.coefficients.wl2(alpha, d = 1, m = m)
expect_true(min(cf$r) > 0)
cf$rel_err
})
expect_true(all(diff(errs) < 0))
}
## m = 6 collapses to the m = 5 fit without the continuation
collapsed <- rational.coefficients.wl2(1.7, d = 1, m = 6, continuation = FALSE)
full <- rational.coefficients.wl2(1.7, d = 1, m = 6)
expect_lt(full$rel_err, collapsed$rel_err)
})
test_that("the NNLS solver is scale invariant and solves the problem", {
set.seed(1)
A <- matrix(abs(rnorm(200 * 5)), 200, 5)
coef <- c(1, 2, 0.5, 3, 0.1)
b <- as.vector(A %*% coef)
expect_equal(rSPDE:::wl2_nnls(A, b), coef, tolerance = 1e-8)
## an absolute tolerance built from A alone would stop early for small b
for (s in c(1e-8, 1e8)) {
expect_equal(rSPDE:::wl2_nnls(A, s * b) / s, coef, tolerance = 1e-8)
}
## the basis of an m = 6 fit spans six orders of magnitude in the poles; the
## solution must not flip when the right hand side is perturbed by a rounding
## error
cells <- rSPDE:::wl2_weyl_cells(1, 1e-26, 900)
theta <- c(3.75, log(c(1.95, 7.6, 42, 324, 4452, 712054)))
Ab <- rSPDE:::wl2_basis("shifted", cells$x, theta) * sqrt(cells$w)
f <- cells$x^1.11 * sqrt(cells$w)
## the true optimum, by enumerating the active sets
brute <- function(A, b) {
n <- ncol(A)
best <- NULL
best_res <- Inf
for (k in 0:(2^n - 1)) {
keep <- as.logical(bitwAnd(k, 2^(0:(n - 1))))
x <- numeric(n)
if (any(keep)) {
## a tight rank tolerance is essential: the columns are strongly
## correlated and the default 1e-7 declares them aliased
cf <- tryCatch(qr.solve(A[, keep, drop = FALSE], b, tol = 1e-14),
error = function(e) NULL
)
if (is.null(cf) || any(cf < 0)) next
x[keep] <- cf
}
res <- sum((as.vector(A %*% x) - b)^2)
if (res < best_res) {
best_res <- res
best <- x
}
}
best
}
sse <- function(A, x, b) sum((as.vector(A %*% x) - b)^2)
r1 <- rSPDE:::wl2_nnls(Ab, f)
expect_equal(r1, brute(Ab, f), tolerance = 1e-6)
## and the solution must not flip when f is perturbed by a rounding error
r2 <- rSPDE:::wl2_nnls(Ab, f * (1 + 1e-15))
expect_equal(r1, r2 / (1 + 1e-15), tolerance = 1e-6)
## the same on bases drawn at random from the region the optimiser explores
set.seed(3)
for (trial in 1:10) {
m <- sample(2:6, 1)
shifted <- sample(c(TRUE, FALSE), 1)
th <- sort(runif(m, 0, 2.2 * m + 6))
if (shifted) th <- c(runif(1, -2, 4), th)
A <- rSPDE:::wl2_basis(if (shifted) "shifted" else "plain", cells$x, th) *
sqrt(cells$w)
b <- cells$x^runif(1, 0.6, 1.9) * sqrt(cells$w)
opt <- brute(A, b)
expect_lte(
sse(A, rSPDE:::wl2_nnls(A, b), b),
sse(A, opt, b) * (1 + 1e-8) + 1e-300
)
}
})
test_that("the shifted class satisfies the stationarity diagnostics", {
## A genuine stationary point of the shifted-factor problem has all residues
## positive, all poles negative, a shift in (0, 1), a vanishing derivative in
## p0, and exactly 2m + 1 sign changes of the error on (0, 1) -- 2m for the
## plain class.
diagnose <- function(cf, alpha, d, m, x_min = NULL) {
x_min <- if (is.null(x_min)) 1e-26 else x_min
n_grid <- if (identical(x_min, 1e-26)) 900 else 300
cells <- rSPDE:::wl2_weyl_cells(d, x_min, n_grid)
e <- rSPDE:::wl2_symbol(cf, cells$x) - cells$x^alpha
p0 <- if (is.null(cf$p0)) 0 else cf$p0
wt <- cells$x^2 / (1 - p0 * cells$x)^2
xs <- exp(seq(log(1e-24), log(1 - 1e-12), length.out = 50001))
es <- rSPDE:::wl2_symbol(cf, xs) - xs^alpha
list(
stationarity = sum(cells$w * e * wt) / sum(cells$w * abs(e) * wt),
sign_changes = sum(diff(sign(es)) != 0)
)
}
## the reference values of the note
cases <- list(
list(alpha = 1.5, d = 2, m = 2, x_min = NULL, err = 2.26e-3, b0 = 0.7951),
list(alpha = 1.5, d = 2, m = 4, x_min = NULL, err = 8.55e-5, b0 = 0.8935),
list(alpha = 1.25, d = 1, m = 3, x_min = NULL, err = 2.10e-4, b0 = 0.9160),
list(alpha = 1.5, d = 2, m = 4, x_min = 1 / 801, err = 3.27e-5, b0 = 0.9047)
)
for (case in cases) {
cf <- rational.coefficients.wl2(case$alpha, case$d, case$m,
x_min = case$x_min
)
expect_equal(cf$rel_err, case$err, tolerance = 0.02)
expect_equal(1 - cf$p0, case$b0, tolerance = 1e-3)
## the feasible set
expect_true(all(cf$r > 0))
expect_true(all(cf$p < 0))
expect_true(cf$p0 > 0 && cf$p0 < 1)
d9 <- diagnose(cf, case$alpha, case$d, case$m, case$x_min)
expect_lt(abs(d9$stationarity), 1e-6)
expect_equal(d9$sign_changes, 2 * case$m + 1)
}
## the sign-change count over the whole class, both integer parts. The
## stationarity condition is a derivative in p0, so it only applies to the
## shifted class; for floor(alpha) = 0 there is no shift.
for (alpha in c(0.7, 0.9, 1.2, 1.8)) {
for (m in c(1, 2, 4)) {
cf <- rational.coefficients.wl2(alpha, d = 1, m = m)
d9 <- diagnose(cf, alpha, 1, m)
expect_equal(d9$sign_changes, 2 * m + (alpha >= 1))
if (!is.null(cf$p0)) {
expect_lt(abs(d9$stationarity), 1e-6)
}
}
}
})
test_that("the shift improves on the unshifted class for floor(alpha) = 1", {
## p0 = 0 is the plain class with the unshifted integer factor, i.e. the form
## the tabulated coefficients use. The note expects a gain of 1.6-2.8.
fit_unshifted <- function(alpha, d, m) {
cells <- rSPDE:::wl2_weyl_cells(d, 1e-26, 900)
x <- cells$x
sw <- sqrt(cells$w)
f <- x^alpha * sw
## no analytic Jacobian for this ad hoc class, so wl2_lm falls back to
## finite differences
res <- function(th, need_jac = FALSE) {
A <- x * rSPDE:::wl2_basis("plain", x, th) * sw
list(r = drop(A %*% rSPDE:::wl2_nnls(A, f)) - f)
}
best <- NULL
for (t0 in rSPDE:::wl2_starts(m, 1e-26, 8L, "plain", NULL, 1L)) {
o <- tryCatch(rSPDE:::wl2_lm(res, t0), error = function(e) NULL)
if (!is.null(o) && (is.null(best) || o$cost < best$cost)) best <- o
}
sqrt(2 * best$cost / sum(cells$w * x^(2 * alpha)))
}
for (alpha in c(1.1, 1.5)) {
for (m in c(1, 2, 4)) {
shifted <- rational.coefficients.wl2(alpha, d = 1, m = m)
expect_gt(fit_unshifted(alpha, 1, m) / shifted$rel_err, 1.5)
expect_true(1 - shifted$p0 > 0.6 && 1 - shifted$p0 < 0.99)
}
}
})
test_that("the analytic Jacobian matches finite differences", {
## Golub-Pereyra derivative of the variable-projection residual, checked at a
## perturbed optimum so that the passive set is the realistic one.
set.seed(1)
for (kind in c("plain", "shifted", "shifted2")) {
for (d in 1:2) {
for (m in c(2, 4, 6)) {
alpha <- switch(kind, plain = 0.8, shifted = 1.4, shifted2 = 2.4)
cells <- rSPDE:::wl2_weyl_cells(d, 1e-26, 900)
sw <- sqrt(cells$w)
f <- cells$x^alpha * sw
theta <- rational.coefficients.wl2(alpha, d, m)$theta +
0.05 * rnorm(m + (rSPDE:::wl2_q(kind) > 0))
a <- rSPDE:::wl2_resjac(kind, cells$x, sw, f, theta)
expect_true(all(a$c > 0))
Jn <- matrix(0, length(cells$x), length(theta))
for (k in seq_along(theta)) {
h <- 1e-7 * max(abs(theta[k]), 1)
tk <- theta
tk[k] <- tk[k] + h
Jn[, k] <- (rSPDE:::wl2_resjac(
kind, cells$x, sw, f, tk, FALSE
)$r - a$r) / h
}
expect_lt(max(abs(a$J - Jn)) / max(abs(Jn)), 1e-4)
}
}
}
})
test_that("the compiled solver agrees with the R implementation", {
## src/wl2_fit.cpp mirrors wl2_nnls(), wl2_resjac() and wl2_lm(); it is used
## whenever it is available, with the R code as the reference and fallback.
skip_if_not(rSPDE:::wl2_have_cpp())
old <- options(rSPDE.wl2.use.cpp = TRUE)
on.exit(options(old), add = TRUE)
cases <- list(
list(alpha = 0.5, d = 1, m = 2, x_min = NULL, type = "covariance"),
list(alpha = 0.75, d = 2, m = 4, x_min = NULL, type = "covariance"),
list(alpha = 1.5, d = 2, m = 2, x_min = NULL, type = "covariance"),
list(alpha = 1.25, d = 1, m = 3, x_min = NULL, type = "covariance"),
list(alpha = 1.5, d = 2, m = 4, x_min = 1 / 801, type = "covariance"),
list(alpha = 2.4, d = 1, m = 2, x_min = NULL, type = "covariance"),
list(alpha = 2.8, d = 2, m = 4, x_min = NULL, type = "covariance"),
list(alpha = 2.4, d = 2, m = 3, x_min = 1 / 801, type = "covariance"),
list(alpha = 1.5, d = 2, m = 3, x_min = 1 / 801, type = "operator")
)
for (cs in cases) {
options(rSPDE.wl2.use.cpp = TRUE)
cf <- do.call(rational.coefficients.wl2, cs)
options(rSPDE.wl2.use.cpp = FALSE)
cr <- do.call(rational.coefficients.wl2, cs)
## The objective is flat near the optimum, so the coefficients may differ
## by more than the error they produce; require both.
expect_equal(cf$rel_err, cr$rel_err, tolerance = 1e-8)
expect_equal(cf$p, cr$p, tolerance = 1e-5)
expect_equal(cf$r, cr$r, tolerance = 1e-5)
if (!is.null(cr$p0)) {
expect_equal(cf$p0, cr$p0, tolerance = 1e-5)
}
}
})
test_that("the non-negative least squares in C matches the R version", {
skip_if_not(rSPDE:::wl2_have_cpp())
## Exercised indirectly above; here directly, on the badly scaled matrices
## the fit actually produces, through a one-iteration fit from a fixed start.
cells <- rSPDE:::wl2_weyl_cells(2, 1e-26, 900)
sw <- sqrt(cells$w)
f <- cells$x^1.4 * sw
theta <- c(1.2, 0.5, 2, 5, 9)
a <- rSPDE:::wl2_resjac("shifted", cells$x, sw, f, theta, FALSE)
expect_true(all(a$c >= 0))
A <- rSPDE:::wl2_basis("shifted", cells$x, theta) * sw
expect_equal(drop(A %*% a$c) - f, a$r, tolerance = 1e-12)
})
test_that("the shipped mesh-free tables cover the expected configurations", {
## Built by data-raw/wl2_tables.R and stored in R/sysdata.rda, because they
## depend on neither the mesh nor kappa and are what matern.operators() uses
## unless a spectral interval is asked for.
tabs <- get0("wl2_meshfree_tables",
envir = asNamespace("rSPDE"), ifnotfound = NULL
)
expect_false(is.null(tabs))
for (d in 1:3) {
for (m_alpha in 0:2) {
for (m in 1:6) {
n_alpha <- length(rSPDE:::wl2_alpha_grid(m_alpha, d, "covariance", 0.01))
tab <- rSPDE:::wl2_shipped_table(d, m, m_alpha, 300)
if (n_alpha == 0) {
## alpha = nu + d / 2 with nu > 0, and the fit needs a finite trace.
expect_null(tab)
next
}
expect_false(is.null(tab))
expect_equal(nrow(tab), n_alpha)
expect_true(all(c(
"alpha", paste0("r", seq_len(m)), paste0("p", seq_len(m)),
"k", "rel_err"
) %in% names(tab)))
expect_equal("p0" %in% names(tab), m_alpha > 0)
## A valid model at every alpha: non-negative residues, poles below one.
expect_true(all(tab[, paste0("r", seq_len(m))] >= 0))
expect_true(all(tab[, paste0("p", seq_len(m))] < 1))
expect_true(all(is.finite(tab$rel_err)))
expect_equal(attr(tab, "x_min"), NULL)
expect_equal(
attr(tab, "kind"),
c("plain", "shifted", "shifted2")[m_alpha + 1]
)
expect_false(is.null(attr(tab, "quad")))
}
}
}
})
test_that("the shipped tables are what the fits produce", {
## Guards against the stored tables going stale: anything that changes the
## coefficients (wl2_fit, wl2_resjac, wl2_nnls, wl2_lm, wl2_weyl_cells, the
## sweep in wl2_coefficient_table, or wl2_tol) means data-raw/wl2_tables.R
## has to be run again. Skipped on CRAN: rebuilding takes half a minute, and
## the last digits of a fit depend on the platform's BLAS, while the stored
## table is the same everywhere.
skip_on_cran()
for (cs in list(c(1, 1, 0), c(2, 2, 1), c(3, 1, 1), c(1, 3, 2), c(2, 2, 2))) {
d <- cs[1]
m <- cs[2]
m_alpha <- cs[3]
rSPDE:::wl2_clear_cache()
fresh <- rSPDE:::wl2_coefficient_table(
d = d, m = m, m_alpha = m_alpha, type = "covariance",
cache = FALSE, shipped = FALSE
)
stored <- rSPDE:::wl2_shipped_table(d, m, m_alpha, 300)
expect_equal(stored$alpha, fresh$alpha)
## The error is the thing that matters and is insensitive to the last
## digits of the poles; the coefficients themselves are checked loosely,
## since the objective is flat at its minimum.
expect_equal(stored$rel_err, fresh$rel_err, tolerance = 1e-6)
for (j in seq_len(m)) {
expect_equal(stored[[paste0("r", j)]], fresh[[paste0("r", j)]],
tolerance = 1e-4
)
expect_equal(stored[[paste0("p", j)]], fresh[[paste0("p", j)]],
tolerance = 1e-4
)
}
if (m_alpha == 1) {
expect_equal(stored$p0, fresh$p0, tolerance = 1e-4)
}
}
})
test_that("a model built from a shipped table equals one built from the fits", {
## The lookup must be transparent: same coefficients, same covariance.
skip_on_cran()
d <- 2
m <- 2
nu <- 0.6
rSPDE:::wl2_clear_cache()
built <- rSPDE:::wl2_coefficient_table(
d = d, m = m, m_alpha = 1, type = "covariance",
cache = FALSE, shipped = FALSE
)
stored <- rSPDE:::wl2_coefficient_table(
d = d, m = m, m_alpha = 1, type = "covariance", cache = FALSE
)
a <- nu + d / 2
cb <- rSPDE:::wl2_interp_coefficients(built, a)
cs <- rSPDE:::wl2_interp_coefficients(stored, a)
expect_equal(cs$r, cb$r, tolerance = 1e-6)
expect_equal(cs$p, cb$p, tolerance = 1e-6)
expect_equal(cs$p0, cb$p0, tolerance = 1e-6)
x <- exp(seq(log(1e-12), 0, length.out = 500))
expect_equal(rSPDE:::wl2_symbol(cs, x), rSPDE:::wl2_symbol(cb, x),
tolerance = 1e-6
)
})
## The columns of a table, without the attributes that record how it was
## asked for rather than what it contains.
wl2_cols <- function(tab) lapply(as.list(tab), as.numeric)
test_that("the persistent cache is off unless it is asked for", {
## A package must not write outside the session temporary directory without
## being told to.
old <- options(rSPDE.cache.dir = NULL)
on.exit(options(old), add = TRUE)
withr::with_envvar(c(RSPDE_CACHE_DIR = NA), {
expect_null(rspde.cache())
expect_null(rSPDE:::wl2_cache_dir())
expect_null(rSPDE:::wl2_cache_paths("covariance", 2, 4, 1, 1 / 801, 8, 300, 0.01))
expect_null(rSPDE:::wl2_cache_read(
"covariance", 2, 4, 1, 1 / 801, 8, 300, 0.01, 300
))
})
withr::with_envvar(c(RSPDE_CACHE_DIR = file.path(tempdir(), "env-cache")), {
expect_equal(rSPDE:::wl2_cache_dir(), file.path(tempdir(), "env-cache"))
})
})
test_that("x_min round-trips through a file name", {
for (x in c(1e-26, 1 / 801, 0.5, 1.97428e-4, 0.999)) {
enc <- rSPDE:::wl2_xmin_encode(x)
expect_false(grepl("[^A-Za-z0-9]", enc))
expect_equal(rSPDE:::wl2_xmin_decode(enc), x, tolerance = 1e-10)
}
expect_equal(rSPDE:::wl2_xmin_encode(NULL), "free")
expect_null(rSPDE:::wl2_xmin_decode("free"))
})
test_that("a cached table is found again, and only when it is valid", {
skip_on_cran()
dir <- file.path(tempdir(), "rspde-cache-test")
unlink(dir, recursive = TRUE)
old <- options(rSPDE.cache.dir = NULL)
on.exit(
{
options(old)
unlink(dir, recursive = TRUE)
},
add = TRUE
)
rspde.cache(dir)
x0 <- 1 / 801
build <- function(x) {
rSPDE:::wl2_clear_cache()
rSPDE:::wl2_coefficient_table(
d = 2, m = 2, m_alpha = 1, x_min = x, type = "covariance"
)
}
first <- build(x0)
again <- build(x0)
expect_equal(wl2_cols(again), wl2_cols(first))
expect_equal(attr(again, "x_min"), x0)
## A table fitted on a slightly wider interval still covers the whole
## spectrum, so it is reused, and says which interval it was fitted on.
near <- build(x0 * 1.003)
expect_equal(wl2_cols(near), wl2_cols(first))
expect_equal(attr(near, "x_min"), x0)
## One fitted on a narrower interval would leave part of the spectrum
## unfitted, and must never be reused.
sharper <- build(x0 * 0.97)
expect_equal(attr(sharper, "x_min"), x0 * 0.97)
## Far enough away it is worth refitting rather than losing the accuracy.
far <- build(x0 * 3)
expect_equal(attr(far, "x_min"), x0 * 3)
expect_gt(length(list.files(dir, recursive = TRUE)), 0)
rspde.cache(clear = TRUE)
expect_equal(length(list.files(dir, recursive = TRUE)), 0)
})
test_that("rspde.wl2.table gives the model the kappa_ref fit", {
skip_on_cran()
old <- options(rSPDE.cache.dir = NULL)
on.exit(options(old), add = TRUE)
mesh <- fmesher::fm_mesh_1d(seq(0, 1, length.out = 101))
nu <- 0.8
kappa_ref <- 5
tab <- rspde.wl2.table(
m = 2, d = 1, nu = nu, kappa_ref = kappa_ref, mesh = mesh
)
expect_equal(
attr(tab, "x_min"),
rspde.xmin(mesh = mesh, kappa_ref = kappa_ref, nu = nu)
)
expect_equal(attr(tab, "d"), 1)
expect_equal(attr(tab, "m"), 2)
expect_equal(attr(tab, "type"), "covariance")
args <- list(
nu = nu, range = 0.3, sigma = 1, mesh = mesh, m = 2, d = 1,
parameterization = "matern", type = "covariance",
type_rational_approximation = "wl2"
)
supplied <- do.call(matern.operators, c(args, list(wl2_table = tab)))
computed <- do.call(matern.operators, c(args, list(kappa_ref = kappa_ref)))
meshfree <- do.call(matern.operators, args)
expect_equal(as.matrix(supplied$Q), as.matrix(computed$Q))
## and it is not simply the default table
expect_false(isTRUE(all.equal(
as.matrix(supplied$Q), as.matrix(meshfree$Q)
)))
## x_min given directly needs no mesh at all. Only the coefficients are
## compared: a table built from kappa_ref also records it.
direct <- rspde.wl2.table(m = 2, d = 1, nu = nu, x_min = attr(tab, "x_min"))
expect_equal(wl2_cols(direct), wl2_cols(tab))
expect_equal(attr(direct, "x_min"), attr(tab, "x_min"))
})
test_that("a table for the wrong model is refused", {
skip_on_cran()
mesh <- fmesher::fm_mesh_1d(seq(0, 1, length.out = 51))
args <- list(
nu = 0.8, range = 0.3, sigma = 1, mesh = mesh, m = 2, d = 1,
parameterization = "matern", type = "covariance",
type_rational_approximation = "wl2"
)
expect_error(
do.call(matern.operators, c(args, list(
wl2_table = rspde.wl2.table(m = 3, d = 1, nu = 0.8, x_min = 1e-3)
))),
"m = 3"
)
expect_error(
do.call(matern.operators, c(args, list(
wl2_table = rspde.wl2.table(m = 2, d = 2, nu = 0.8, x_min = 1e-3)
))),
"d = 2"
)
expect_error(
do.call(matern.operators, c(args, list(wl2_table = data.frame(alpha = 1)))),
"rspde.wl2.table"
)
expect_error(rspde.wl2.table(m = 2, d = 1, nu = 0.8), "x_min")
expect_error(rspde.wl2.table(m = 2, d = 1), "alpha or nu")
})
test_that("the data weight x^s is the one it claims to be", {
## wl2_weyl_cells returns cell integrals of w_d(x) x^s, taken in log x, so
## the integrand there is x^(s - d/2) (1-x)^(d/2-1).
for (d in 1:2) {
for (s in c(0, 0.5, 1)) {
n <- 200
cl <- rSPDE:::wl2_weyl_cells(d, 1e-3, n, s = s)
lx <- seq(log(1e-3), 0, length.out = n)
ed <- c(lx[1], (lx[-1] + lx[-n]) / 2, 0)
## The last cells are dropped: for d = 1 the density has a (1-x)^(-1/2)
## singularity at x = 1, where the midpoint rule of wl2_weyl_cells and
## integrate() differ by more than the identity being checked.
keep <- seq_len(n - 3)
ref <- vapply(keep, function(i) {
stats::integrate(function(u) {
xx <- exp(u)
xx^(s - d / 2) * (1 - xx)^(d / 2 - 1)
}, ed[i], ed[i + 1], rel.tol = 1e-10)$value
}, numeric(1))
expect_equal(cl$w[keep], ref, tolerance = 1e-6)
}
}
## s = 0 is the default, and changes nothing
expect_equal(
rSPDE:::wl2_weyl_cells(2, 1e-4, 100),
rSPDE:::wl2_weyl_cells(2, 1e-4, 100, s = 0)
)
expect_equal(
rational.coefficients.wl2(1.5, 2, 3),
rational.coefficients.wl2(1.5, 2, 3, s = 0, k_term = FALSE)
)
})
test_that("the fit minimises the weighted objective it is given", {
## Checked against an independent quadrature ten times finer than the fit's.
obj <- function(cf, alpha, d, x_min, s) {
cl <- rSPDE:::wl2_weyl_cells(d, x_min, 3000, s = s)
e <- rSPDE:::wl2_symbol(cf, cl$x) - cl$x^alpha
sqrt(sum(cl$w * e^2) / sum(cl$w * cl$x^(2 * alpha)))
}
for (cs in list(
list(a = 0.75, d = 2, m = 3, x = 1e-3, s = 0),
list(a = 0.75, d = 2, m = 3, x = 1e-3, s = 1),
list(a = 0.6, d = 2, m = 2, x = 1e-4, s = 0.5)
)) {
for (kt in c(FALSE, TRUE)) {
cf <- rational.coefficients.wl2(
cs$a, cs$d, cs$m, x_min = cs$x, s = cs$s, k_term = kt
)
expect_equal(cf$rel_err, obj(cf, cs$a, cs$d, cs$x, cs$s),
tolerance = 5e-3
)
expect_equal(cf$k > 0, kt)
expect_true(all(cf$r > 0))
expect_true(all(cf$p < 1))
}
}
})
test_that("the constant term helps, and leaves on its own when it may not stay", {
## On (0, 1] with s = 0 the integral of k^2 w_d diverges, so the optimal
## constant is zero; on a finite interval it is not, and it is worth having.
gain <- function(x_min) {
a <- rational.coefficients.wl2(0.75, 2, 3, x_min = x_min, k_term = FALSE)
b <- rational.coefficients.wl2(0.75, 2, 3, x_min = x_min, k_term = TRUE)
c(gain = a$rel_err / b$rel_err, k = b$k)
}
wide <- gain(1e-2)
mid <- gain(1e-4)
free <- gain(1e-26)
expect_gt(wide[["gain"]], 2)
expect_gt(mid[["gain"]], 1.5)
expect_equal(free[["gain"]], 1, tolerance = 1e-6)
## and the constant shrinks with the interval
expect_gt(wide[["k"]], mid[["k"]])
expect_lt(free[["k"]], 1e-12)
})
test_that("a constant term is refused where the model cannot represent it", {
## The operator factorisation has no constant term, so coefficients fitted
## with one must not be silently turned into an operator-based model.
cf <- rational.coefficients.wl2(0.75, 2, 2, x_min = 1e-3, k_term = TRUE)
expect_gt(cf$k, 0)
expect_error(rSPDE:::wl2_roots(cf), "constant term")
expect_silent(rSPDE:::wl2_roots(
rational.coefficients.wl2(1.5, 2, 2, x_min = 1e-3, type = "operator")
))
})
test_that("the objective must be finite", {
## Near zero the integrand is e(x)^2 x^(s - 1 - d/2) with e ~ x^alpha, so
## 2 alpha + s > d / 2 is needed. The weight can make a fit possible that is
## not possible without it.
expect_error(rational.coefficients.wl2(0.4, 2, 2, x_min = 1e-3), "finite")
expect_silent(rational.coefficients.wl2(0.4, 2, 2, x_min = 1e-3, s = 0.5))
expect_error(rational.coefficients.wl2(0.75, 2, 2, s = "a"), "single finite")
})
test_that("the compiled solver handles the constant term too", {
skip_if_not(rSPDE:::wl2_have_cpp())
old <- options(rSPDE.wl2.use.cpp = TRUE)
on.exit(options(old), add = TRUE)
for (cs in list(
list(a = 0.75, d = 2, m = 3, x = 1e-3, s = 0),
list(a = 0.75, d = 2, m = 4, x = 1e-3, s = 1),
list(a = 0.9, d = 2, m = 3, x = 1e-5, s = 1)
)) {
options(rSPDE.wl2.use.cpp = TRUE)
cc <- rational.coefficients.wl2(
cs$a, cs$d, cs$m, x_min = cs$x, s = cs$s, k_term = TRUE
)
options(rSPDE.wl2.use.cpp = FALSE)
rr <- rational.coefficients.wl2(
cs$a, cs$d, cs$m, x_min = cs$x, s = cs$s, k_term = TRUE
)
expect_equal(cc$rel_err, rr$rel_err, tolerance = 1e-8)
expect_equal(cc$k, rr$k, tolerance = 1e-5)
expect_equal(cc$p, rr$p, tolerance = 1e-5)
}
})
test_that("a table can carry the weight and the constant term", {
## The options of rational.coefficients.wl2() have to be reachable at the
## table level too, otherwise they can only be used one alpha at a time.
old <- options(rSPDE.cache.dir = NULL)
on.exit(options(old), add = TRUE)
for (cs in list(
list(s = 0, k = FALSE), list(s = 0, k = TRUE), list(s = 1, k = TRUE)
)) {
rSPDE:::wl2_clear_cache()
tab <- rSPDE:::wl2_coefficient_table(
d = 1, m = 2, m_alpha = 0, x_min = 1e-3, type = "covariance",
s = cs$s, k_term = cs$k, by = 0.05
)
expect_equal(attr(tab, "s"), cs$s)
expect_equal(attr(tab, "k_term"), cs$k)
expect_equal(any(tab$k > 0), cs$k)
## the interpolation must carry the constant term through and stay as good
## as a direct fit at the same alpha
cf <- rSPDE:::wl2_interp_coefficients(tab, 0.775)
direct <- rational.coefficients.wl2(
0.775, 1, 2, x_min = 1e-3, s = cs$s, k_term = cs$k
)
expect_equal(cf$k > 0, cs$k)
expect_equal(cf$rel_err, direct$rel_err, tolerance = 1e-3)
}
})
test_that("stored tables are never reused for a different weight", {
old <- options(rSPDE.cache.dir = NULL)
on.exit(options(old), add = TRUE)
rSPDE:::wl2_clear_cache()
a <- rSPDE:::wl2_coefficient_table(d = 2, m = 2, m_alpha = 1,
type = "covariance")
b <- rSPDE:::wl2_coefficient_table(d = 2, m = 2, m_alpha = 1,
type = "covariance", s = 1)
expect_equal(attr(a, "s"), 0)
expect_equal(attr(b, "s"), 1)
expect_false(isTRUE(all.equal(a$rel_err, b$rel_err)))
## and the cache paths differ
p0 <- rSPDE:::wl2_cache_paths("covariance", 2, 2, 1, 1e-3, 8, 300, 0.01)
p1 <- rSPDE:::wl2_cache_paths("covariance", 2, 2, 1, 1e-3, 8, 300, 0.01,
s = 1, k_term = TRUE)
expect_null(p0)
expect_null(p1)
withr::with_options(
list(rSPDE.cache.dir = file.path(tempdir(), "wcache")), {
q0 <- rSPDE:::wl2_cache_paths("covariance", 2, 2, 1, 1e-3, 8, 300, 0.01)
q1 <- rSPDE:::wl2_cache_paths("covariance", 2, 2, 1, 1e-3, 8, 300, 0.01,
s = 1, k_term = TRUE)
expect_false(identical(q0$dir, q1$dir))
}
)
})
test_that("the largest eigenvalue is computed without RSpectra", {
## rspde.xmin(eigenvalue = "exact") used RSpectra::eigs_sym(), which fails to
## converge on exactly the meshes it is wanted for -- a 1d mesh with 2001
## nodes returned nothing at all, and the zero-length result propagated
## silently into x_min. Lanczos handles it in a hundredth of a second.
for (cfg in list(
list(mesh = fmesher::fm_mesh_1d(seq(0, 1, length.out = 401))),
list(mesh = fmesher::fm_mesh_1d(seq(0, 1, length.out = 2001)))
)) {
fe <- fmesher::fm_fem(cfg$mesh)
cl <- Matrix::rowSums(fe$c0)
Ci2 <- Matrix::Diagonal(length(cl), 1 / sqrt(cl))
S <- Ci2 %*% fe$g1 %*% Ci2
got <- rSPDE:::wl2_lambda_max(S)
expect_length(got, 1)
expect_true(is.finite(got))
ref <- max(eigen(as.matrix((S + Matrix::t(S)) / 2),
symmetric = TRUE, only.values = TRUE
)$values)
## converges from below, and slowly in one dimension where the top of the
## spectrum is clustered: 1.6e-3 at the default twenty steps
expect_lt(abs(got / ref - 1), 5e-3)
expect_lte(got, ref * (1 + 1e-10))
}
## and through the user-facing function, where the result must be a single
## number in (0, 1) and at most the Gershgorin bound's interval
for (n in c(401, 2001)) {
mesh <- fmesher::fm_mesh_1d(seq(0, 1, length.out = n))
xe <- rspde.xmin(mesh = mesh, kappa_ref = 5, nu = 0.5, eigenvalue = "exact")
xb <- rspde.xmin(mesh = mesh, kappa_ref = 5, nu = 0.5)
expect_length(xe, 1)
expect_true(is.finite(xe) && xe > 0 && xe < 1)
## the bound over-estimates the largest eigenvalue, so it under-estimates
## x_min; the exact value is never smaller, and never larger than the truth
## allows, since rspde.xmin caps it at the bound after a 1% margin
expect_gte(xe, xb * (1 - 1e-8))
}
## a small matrix falls through to a dense decomposition
S <- Matrix::Diagonal(5, c(1, 2, 3, 4, 5))
expect_equal(rSPDE:::wl2_lambda_max(S), 5)
})
test_that("the intrinsic scaling is computed without RSpectra", {
## The scaling of the intrinsic models is the smallest non-zero eigenvalue of
## C^{-1/2} G C^{-1/2}. Its null vector is known rather than computed, since
## G is a stiffness matrix and G 1 = 0, so deflating it turns the second
## smallest eigenvalue into the smallest and shift-and-invert Lanczos reaches
## it in about a dozen steps.
skip_if_not_installed("fmesher")
set.seed(1)
mesh <- fmesher::fm_mesh_2d(
loc = cbind(runif(150), runif(150)), max.edge = c(0.08, 0.24)
)
fe <- fmesher::fm_fem(mesh)
C <- fe$c0
G <- fe$g1
n <- dim(C)[1]
## the assumption the deflation rests on
expect_lt(max(abs(as.vector(G %*% rep(1, n)))), 1e-10 * max(abs(Matrix::diag(G))))
Cd <- Matrix::Diagonal(n, 1 / sqrt(Matrix::diag(C)))
Gg <- as(Cd %*% G %*% Cd, "CsparseMatrix")
got <- rSPDE:::rspde_lambda_min_nonzero(Gg, sqrt(Matrix::diag(C)))
ref <- sort(eigen(as.matrix((Gg + Matrix::t(Gg)) / 2),
symmetric = TRUE, only.values = TRUE
)$values)[2]
expect_equal(got, ref, tolerance = 1e-8)
## and through the model, where it is the scaling
op <- intrinsic.operators(C = C, G = G, beta = 1, tau = 1, d = 2, m = 2)
expect_equal(op$scaling, ref, tolerance = 1e-8)
## a supplied scaling still wins, and opts is honoured rather than ignored
expect_equal(
intrinsic.operators(C = C, G = G, beta = 1, tau = 1, d = 2, m = 2,
scaling = 4.6)$scaling, 4.6
)
expect_equal(
intrinsic.operators(C = C, G = G, beta = 1, tau = 1, d = 2, m = 2,
opts = list(tol = 1e-6, maxitr = 100))$scaling,
ref, tolerance = 1e-5
)
## a vector that is not a null vector is refused rather than silently used
expect_true(is.na(rSPDE:::rspde_lambda_min_nonzero(Gg, seq_len(n))))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.