Nothing
#' Create Time Series Cross-Validation Indices
#'
#' @description Creates indices for time series cross-validation with options for expanding window
#' or sliding window approaches. Supports both single-step and multi-step forecasting, and can
#' handle replicated observations.
#'
#' @param time_idx A numeric vector of time indices in ascending order
#' @param train_length An integer specifying the fixed length of training sets.
#' If NULL (default), an expanding window approach is used.
#' @param test_length An integer specifying the number of observations to include in each test set.
#' Default is 1 (single-step forecasting).
#' @param replicate An optional vector of the same length as time_idx, indicating which observations
#' belong to the same replicate group. When provided, ensures that all observations with the
#' same replicate value are either entirely in the training set or entirely in the test set.
#' @param gap An integer specifying the gap between the training set and test set.
#' Default is 0 (no gap, test set starts immediately after training set).
#' For example, gap=1 means skip one time point between training and test (useful for 2-step ahead forecasting).
#'
#' @return A list with two components:
#' \describe{
#' \item{train}{A list of numeric vectors, where each vector contains the time indices for training in that fold}
#' \item{test}{A list of numeric vectors, where each vector contains the time indices for testing in that fold}
#' }
#'
#' @details
#' Time series cross-validation requires respecting the temporal order of observations.
#' This function implements two common approaches:
#'
#' 1. Expanding window (when train_length = NULL): The training set grows with each fold,
#' starting with a minimal set and expanding to include all but the test data.
#'
#' 2. Sliding window (when train_length is specified): Uses a fixed-length window
#' that slides through the time series, maintaining the same training size across folds.
#'
#' The test_length parameter allows for multi-step forecasting evaluation.
#'
#' When replicate is provided, the function ensures that all observations with the same
#' replicate value are kept together, either all in the training set or all in the test set.
#' This is useful for scenarios where multiple observations at the same time point should
#' be treated as a group.
#'
#' The gap parameter creates a separation between training and test sets, which is useful
#' for multi-step ahead forecasting validation.
#'
#' @examples
#' # Expanding window approach with single-step forecasting
#' cv_expanding <- make_time_series_cv_index(1:10)
#'
#' # Sliding window approach with window size 3 and single-step forecasting
#' cv_sliding <- make_time_series_cv_index(1:10, train_length = 3)
#'
#' # Sliding window with multi-step forecasting (predict 2 steps ahead)
#' cv_multistep <- make_time_series_cv_index(1:10, train_length = 3, test_length = 2)
#'
#' # Working with replicates
#' time_idx <- c(1, 1, 1, 2, 2, 3, 3)
#' replicates <- c(1, 1, 1, 2, 2, 3, 3)
#' cv_with_replicates <- make_time_series_cv_index(time_idx, replicate = replicates)
#'
#' # 2-step ahead forecasting with a gap
#' cv_with_gap <- make_time_series_cv_index(1:10, gap = 1)
#'
#' @export
make_time_series_cv_index <- function(
time_idx,
train_length = NULL,
test_length = 1,
replicate = time_idx,
gap = 0) {
# Validate inputs
if (!is.numeric(time_idx)) {
stop("time_idx must be numeric")
}
n <- length(time_idx)
if (n < 2) {
stop("time_idx must have at least 2 elements")
}
# Validate test_length
if (test_length < 1) {
stop("test_length must be at least 1")
}
# Validate gap
if (gap < 0) {
stop("gap must be non-negative")
}
# Initialize lists for train and test indices
train_indices <- list()
test_indices <- list()
# Validate and process replicate information if provided
if (!is.null(replicate)) {
if (length(replicate) != length(time_idx)) {
stop("replicate must have the same length as time_idx")
}
# Create a data frame to keep track of time points and their replicates
data <- data.frame(
idx = 1:length(time_idx),
time = time_idx,
replicate = replicate
)
# Sort by time and then by replicate to ensure proper ordering
data <- data[order(data$time, data$replicate), ]
# Get unique replicates in order of their first appearance
unique_replicates <- unique(data$replicate)
replicate_first_times <- tapply(data$time, data$replicate, min)
replicate_order <- order(replicate_first_times)
sorted_unique_replicates <- unique_replicates[replicate_order]
# We'll work with replicate groups instead of individual time points
time_groups <- sort(unique(replicate_first_times))
n_groups <- length(time_groups)
# Time series CV with replicates - we'll work with groups
if (is.null(train_length)) {
# Expanding window mode with replicates
# We'll create folds by using all earlier replicates for training and later ones for testing
# Calculate the maximum number of folds considering the gap
max_folds <- n_groups - gap - test_length
# Ensure we have at least one fold
if (max_folds < 1) {
stop("Not enough replicate groups for the specified gap and test_length")
}
# For each possible train/test split of replicate groups
for (i in 1:max_folds) {
# Identify which replicates are in training set
train_replicates <- sorted_unique_replicates[1:i]
# Get all rows where the replicate is in the training set
train_idx <- data$idx[data$replicate %in% train_replicates]
train_indices[[i]] <- time_idx[train_idx]
# Get the test replicates after the gap
test_replicates <- sorted_unique_replicates[
(i + 1 + gap):min(i + gap + test_length, n_groups)
]
# Get all rows where the replicate is in the test set
test_idx <- data$idx[data$replicate %in% test_replicates]
test_indices[[i]] <- time_idx[test_idx]
}
} else {
# Fixed-length sliding window with replicates
# Validate train_length - now in terms of number of replicate groups
if (train_length <= 0 || train_length >= n_groups) {
stop("train_length must be between 1 and the number of replicate groups - 1")
}
# The maximum number of folds we can create, accounting for the gap
max_folds <- n_groups - train_length - gap - test_length + 1
# Ensure we have at least one fold
if (max_folds < 1) {
stop("Not enough replicate groups for the specified train_length, gap, and test_length")
}
# Generate sliding window CV folds based on replicate groups
for (i in 1:max_folds) {
# Training set: replicates from position i to i+train_length-1
train_replicates <- sorted_unique_replicates[i:(i + train_length - 1)]
# Get all rows where the replicate is in the training set
train_idx <- data$idx[data$replicate %in% train_replicates]
train_indices[[i]] <- time_idx[train_idx]
# Test set: the next test_length replicates after the gap
test_replicates <- sorted_unique_replicates[
(i + train_length + gap):(min(i + train_length + gap + test_length - 1, n_groups))
]
# Get all rows where the replicate is in the test set
test_idx <- data$idx[data$replicate %in% test_replicates]
test_indices[[i]] <- time_idx[test_idx]
}
}
} else {
# Standard time series CV without replicates
# Sort time indices to ensure chronological order if they aren't already
if (!all(diff(time_idx) >= 0)) {
time_idx <- sort(unique(time_idx))
n <- length(time_idx)
}
if (is.null(train_length)) {
# Expanding window mode (default behavior)
# We'll create as many folds as possible while ensuring enough test points
# and accounting for the gap
max_folds <- n - gap - test_length
if (max_folds < 1) {
stop("Not enough data points for the specified gap and test_length")
}
for (i in 1:max_folds) {
# Training set: indices from 1 to i
train_indices[[i]] <- time_idx[1:i]
# Test set: observations after training and gap
test_start <- i + 1 + gap
test_end <- min(test_start + test_length - 1, n)
test_indices[[i]] <- time_idx[test_start:test_end]
}
} else {
# Fixed-length sliding window mode
# Validate train_length
if (train_length <= 0 || train_length >= n) {
stop("train_length must be between 1 and length(time_idx)-1")
}
# The maximum number of folds we can create, accounting for the gap
max_folds <- n - train_length - gap - test_length + 1
# Ensure we have at least one fold
if (max_folds < 1) {
stop("Not enough data points for the specified train_length, gap, and test_length")
}
# Generate sliding window CV folds
for (i in 1:max_folds) {
# For sliding window: start with indices i:(i+train_length-1)
start_idx <- i
end_idx <- i + train_length - 1
# Training set: window of length train_length
train_indices[[i]] <- time_idx[start_idx:end_idx]
# Test set: observations after training and gap
test_start <- end_idx + 1 + gap
test_end <- min(test_start + test_length - 1, n)
test_indices[[i]] <- time_idx[test_start:test_end]
}
}
}
# Return the result as a list containing train and test indices
return(list(
train = train_indices,
test = test_indices
))
}
#' Convert sparse matrix into sparse dgCMatrix
#' @param G matrix
#'
#' @return sparse dgCMatrix
#' @export
ngme_as_sparse <- function(G) {
tryCatch(
expr = {
G <- as(as(G, "CsparseMatrix"), "generalMatrix")
# G <- as(as(G, "dMatrix"), "generalMatrix")
},
error = function(e) {
G <- as(G, "dgTMatrix")
idx <- which(G@i <= G@j)
G <- Matrix::sparseMatrix(
i = G@i[idx], j = G@j[idx], x = G@x[idx],
symmetric = FALSE, index1 = FALSE
)
G <- as(as(G, "CsparseMatrix"), "generalMatrix")
# G <- as(as(G, "dMatrix"), "generalMatrix")
},
finally = {
G
}
)
}
#' @name get_inla_mesh_dimension
#' @title Get the dimension of an INLA mesh
#' @description Get the dimension of an INLA mesh
#' @param inla_mesh An INLA mesh
#' @return The dimension of an INLA mesh.
#' @noRd
#'
get_inla_mesh_dimension <- function(inla_mesh) {
cond1 <- inherits(inla_mesh, "inla.mesh.1d")
cond2 <- inherits(inla_mesh, "inla.mesh")
stopifnot(cond1 || cond2)
if (inla_mesh$manifold == "R1") {
d <- 1
} else if (inla_mesh$manifold %in% c("R2", "S2")) {
d <- 2
} else {
stop("The mesh should be from a flat manifold.")
}
return(d)
}
# format output
ngme_format <- function(param, val, model = NULL, ...) {
stationary <- (length(val) == 1)
dne <- (length(val) == 0)
dots <- list(...)
nu_lower_bound <- if (!is.null(dots$nu_lower_bound)) dots$nu_lower_bound else 0
if (is.null(model)) { # noise
if (stationary) {
val <- if (grepl("sigma", param, fixed = TRUE)) {
format(exp(val), digits = 3)
} else if (grepl("nu", param, fixed = TRUE)) {
format(nu_lower_bound + exp(val), digits = 3)
} else {
format(val, digits = 3)
}
} else {
val <- paste0(format(val, digits = 3), collapse = ", ")
}
switch(param,
"sigma" = if (stationary) {
paste0("sigma = ", val)
} else {
paste0("theta_sigma = ", val)
},
"sigma_nig" = if (stationary) {
paste0("sigma_nig = ", val)
} else {
paste0("theta_sigma_nig = ", val)
},
"sigma_normal" = if (stationary) {
paste0("sigma_normal = ", val)
} else {
paste0("theta_sigma_normal = ", val)
},
"mu" = if (stationary) {
paste0("mu = ", val)
} else {
paste0("theta_mu = ", val)
},
"nu" = if (stationary) {
paste0("nu = ", val)
} else {
paste0("theta_nu = ", val)
},
"feff" = if (dne) "No fixed effects" else paste0("feff = ", val)
)
} else { # model
switch(model,
"ar1" = paste0("alpha = ", format(ar1_th2a(val), digits = 3)),
"matern" = paste0("theta_kappa = ", paste0(format(val, digits = 3), collapse = ", ")),
"ou" = paste0("theta_K = ", paste0(format(val, digits = 3), collapse = ", ")),
"re" = {
paste(capture.output(vecK_to_Sigma(val, list(...)[[1]])), collapse = "\n")
}
)
}
}
#' taking mean over a list of nested lists
#'
#' @param lls a list
#' @param weights weights of each list
#'
#' @return a list of nested lists
#' @export
#'
#' @examples
#' ls <- list(
#' list(a = 1, b = 2, t = "nig", ll = list(a = 1, b = 2, w = "ab")),
#' list(a = 3, b = 5, t = "nig", ll = list(a = 1, b = 6, w = "ab")),
#' list(a = 5, b = 5, t = "nig", ll = list(a = 4, b = 2, w = "ab"))
#' )
#' mean_list(ls)
mean_list <- function(lls, weights = NULL) {
n <- length(lls)
weights <- if (is.null(weights)) {
rep(1 / n, n)
} else {
weights / sum(weights)
}
# helpers
nest_list_add <- function(l1, l2) {
for (i in seq_along(l2)) {
if (is.numeric(l2[[i]])) {
l1[[i]] <- l1[[i]] + l2[[i]]
}
if (is.list(l2[[i]])) {
l1[[i]] <- nest_list_add(l1[[i]], l2[[i]])
}
}
l1
}
nest_list_mult <- function(l, n) {
for (i in seq_along(l)) {
if (is.numeric(l[[i]])) l[[i]] <- l[[i]] * n
if (is.list(l[[i]])) l[[i]] <- nest_list_mult(l[[i]], n)
}
l
}
ret <- nest_list_mult(lls[[1]], 0)
for (i in seq_along(lls)) {
tmp <- nest_list_mult(lls[[i]], weights[[i]])
ret <- nest_list_add(ret, tmp)
}
ret
}
# helper functions
# ar1 alpha (-1~1) to theta_K
ar1_a2th <- function(a) {
log((-1 - a) / (-1 + a))
}
# theta_K to ar1 alpha (0~1)
ar1_th2a <- function(th) {
-1 + (2 * exp(th)) / (1 + exp(th))
}
# #' Make index for the matern model
# #'
# #' @param name
# #' @param n.spde
# #' @param n.repl
# #' @param mesh
# #' @param dim
# #'
# #' @return
# #' @export
# #'
# #' @examples
# ngme.matern.make.index <- function(
# n.spde=NULL,
# n.repl = 1,
# mesh = NULL,
# dim = NULL
# ){
# if(is.null(n.spde)&&is.null(mesh)){
# stop("You should provide either n.spde or mesh!")
# }
# if(!is.null(mesh)){
# n_mesh = mesh$n
# if(mesh$manifold == "R1"){
# dim = 1
# } else if(mesh$manifold == "R2"){
# dim = 2
# } else{
# stop("The domain must be flat manifolds of dimension 1 or 2, that is,
# the domain must be a line or a plane.")
# }
# } else{
# n_mesh <- n.spde
# if(is.null(dim)) {
# stop("You should provide the dimension d!")
# }
# }
# out <- list()
# out$index <- rep(1:n_mesh, times = n.repl)
# out$replicate <- rep(1:n.repl, each = n_mesh)
# return(out)
# }
#' Make observation matrix for time series
#'
#' @param loc integers (after sorting, no gaps > 1)
#' @param replicate indicating replicate measure at same location
#' @param range range for the mesh
#' by default range=(min(loc), max(loc))
#'
#' @return A matrix (length(loc) * length(unique(loc)))
#' @export
#'
#' @examples
#' ngme_ts_make_A(c(1, 2, 2), replicate = c(1, 1, 2))
#' ngme_ts_make_A(c(1, 2, 2), range = c(1, 5))
ngme_ts_make_A <- function(
loc,
replicate = NULL,
range = c(min(loc), max(loc))) {
if (is.null(loc) || length(loc) == 0) {
return(NULL)
}
n_loc <- length(loc)
nrep <- 1
start <- range[1]
end <- range[2]
n_range <- end - start + 1
if (is.null(replicate)) {
replicate <- rep(1, n_range)
}
unique_rep <- unique(replicate)
nrep <- length(unique_rep)
A <- matrix(0, nrow = n_loc, ncol = n_range * nrep)
for (i in 1:n_loc) {
ncol_rep <- which(unique_rep == replicate[i])
A[i, (ncol_rep - 1) * n_range + loc[i] - start + 1] <- 1
}
# as(A, "dgCMatrix")
as(as(A, "dMatrix"), "generalMatrix")
}
# compute the mode of data
emprical_mode <- function(x, breaks = max(20, length(x) / 20)) {
h <- hist(x, breaks = breaks, plot = FALSE)
idx <- which.max(h$counts)
h$mids[idx]
}
# build <- function(A1, A2) {
# }
# t, (x, y)
# f(mesh = list(mesh1, mesh2), replicate=112233) + f(ar1 )
# given a list of replicate
# merge_repls <- function(repls) {
# # merge the list of data frames
# # input: list of numerics
# # output: merged list
# # assert of equal length
# stopifnot(length(unique(as.numeric(lapply(repls, length)))) == 1)
# # helper function of merge 2 repls
# merge_repl <- function(repl, group) {
# halas <- FALSE
# while (!halas) {
# unique_group <- unique(group)
# halas <- TRUE
# if (length(unique_group) == 1) return (group)
# for (i in 1:(length(unique_group)-1)) {
# for (j in (i+1):length(unique_group)) {
# if (length(intersect(repl[group == unique_group[i]], repl[group == unique_group[j]])) > 0) {
# group[group == unique_group[j]] <- unique_group[i]
# halas <- FALSE
# }
# if (!halas) break
# }
# if (!halas) break
# }
# }
# group
# }
# Reduce(function(x, y) merge_repl(x, y), repls)
# }
split_matrix <- function(mat, repl) {
split_mat <- lapply(split(mat, repl, drop = FALSE),
matrix,
ncol = ncol(mat)
)
split_mat
}
# helper function to unfiy way of accessing 1d and 2d index
as_map <- function(locs) {
if (inherits(locs, c("data.frame", "matrix"))) {
as.matrix(locs)
} else {
as.numeric(locs)
}
}
sub_map <- function(locs, idx) {
if (inherits(locs, c("data.frame", "matrix"))) {
locs[idx, , drop = FALSE]
} else {
locs[idx]
}
}
dim_map <- function(map) {
if (inherits(map, c("data.frame", "matrix"))) ncol(map) else 1
}
rep_map <- function(map, times) {
if (inherits(map, c("data.frame", "matrix"))) {
do.call(rbind, replicate(times, map, simplify = FALSE))
} else {
rep(map, times = times)
}
}
length_map <- function(map) {
if (inherits(map, c("data.frame", "matrix"))) {
nrow(map)
} else {
length(map)
}
}
# vectorize a matrix
veci <- function(v, n, m) {
if (length(v) != n * m) {
stop(
"Wrong dimensions in reshape: ", length(v),
" (", n, ", ", m, ")",
call. = FALSE
)
}
M <- matrix(0, nrow = n, ncol = m)
count <- 1
for (i in 1:m) {
M[, i] <- v[count:(count + n - 1)]
count <- count + n
}
return(M)
}
# M <- matrix(c(1,2,3,2,4,5,3,5,6), 3, 3); M
vech <- function(M) {
stopifnot(nrow(M) == ncol(M))
n <- nrow(M)
V <- double(n * (n - 1) / 2 + n)
V[1:n] <- diag(M)
k <- n + 1
for (i in seq_len(n - 1)) {
V[k:(k + n - i - 1)] <- tail(M[, i], n - i)
k <- k + n - i
}
return(V)
}
vech_to_mat <- function(vech, n) {
stopifnot(n * (n - 1) / 2 + n == length(vech))
mat <- matrix(0, nrow = n, ncol = n)
mat[lower.tri(mat)] <- tail(vech, length(vech) - n)
mat <- mat + t(mat)
diag(mat) <- vech[1:n]
mat
}
vecK_to_Sigma <- function(theta_K, n) {
tmp <- theta_K
tmp[1:n] <- exp(theta_K[1:n])
K <- vech_to_mat(tmp, n)
Kinv <- solve(K)
Sigma <- Kinv %*% t(Kinv)
Sigma
}
build_D <- function(theta, rho) {
d11 <- cos(theta) + rho * sin(theta)
d12 <- -sin(theta) * (sqrt(1 + rho^2))
d21 <- sin(theta) - rho * cos(theta)
d22 <- cos(theta) * (sqrt(1 + rho^2))
matrix(c(d11, d21, d12, d22), nrow = 2, ncol = 2)
}
build_effect_K <- function(n_reff, theta_K) {
n_theta_K <- sum(1:n_reff)
K <- diag(n_reff)
diag(K) <- exp(theta_K[1:n_reff])
if (n_reff > 1) {
K[lower.tri(K)] <- theta_K[(n_reff + 1):n_theta_K]
}
K
}
# given a basis matrix, find if it is stationary
is_stationary <- function(B) {
stopifnot(is.matrix(B))
ncol(B) == 1 && all(B == 1)
}
#' @title ngme make mesh for different replicates
#' @description
#' Make different mesh for different replicates
#'
#' @param data provide the data.frame
#' @param map provide the map to make mesh, i.g. ~x+y, x and y will be extracted from data to make a 2d mesh
#' @param replicate provide the replicate information, i.g. ~id
#' @param mesh_type type of mesh, "regular" means use all the point from same replicate to make a mesh
#'
#' @return a list of mesh of length of different replicates
#' @export
ngme_make_mesh_repls <- function(
data,
map,
replicate,
mesh_type = "regular") {
if (inherits(map, "formula")) {
map <- model.matrix(map, data)[, -1]
}
if (inherits(replicate, "formula")) {
replicate <- model.matrix(replicate, data)[, -1]
}
stopifnot(length_map(map) == length(replicate))
mesh_repls <- NULL
for (repl in replicate) {
map_repl <- subset(map, replicate == repl)
if (dim_map(map) == 1) {
mesh_repls[[as.character(repl)]] <- tryCatch(
fmesher::fm_mesh_1d(map_repl),
error = function(e) {
stop("The nodes for making mesh is not valid for replicate id=", repl)
}
)
} else if (dim_map(map) == 2) {
stop("Not implemented yet.")
mesh_repls[[repl]] <- fmesher::fm_mesh_2d(map_repl)
} else {
stop("The dimension of the mesh should be 1 or 2.")
}
}
mesh_repls
}
# check if group is valid
# return as integer
validate_rep_or_group <- function(replicate, data) {
if (is.null(data)) {
return(as.factor(replicate))
}
if (is.null(replicate)) {
replicate <- rep(1, nrow(data))
}
if (inherits(replicate, "formula")) {
# input as: replicate = ~id
stopifnot(
"Allow 1 variable (column in data) as replicate. i.g. replicate=~id" = length(replicate) == 2 && length(replicate[[2]]) == 1
)
replicate <- eval(replicate[[2]], envir = data, enclos = parent.frame())
}
if (inherits(replicate, "character") && (length(replicate) == 1)) {
# input as: replicate = "id"
replicate <- data[[replicate]]
}
stopifnot(
"Please make sure the length of replicate/group is equal to the number of rows of data" = nrow(data) == length(replicate)
)
return(as.factor(replicate))
}
# #' @title Use more interpretable parameterization of the matern model
# #' @description
# #' From SPDE parameter (kappa, sigma, alpha) to (theta_kappa, theta_sigma, theta_alpha)
# #'
# #' @param ope provide the operator
# #'
# #' @return a list of mesh of length of different replicates
# #' @export
# matern_result <- function(ope) {
# stop("to-do")
# }
# return the relative idx among all latent models
# using W_size assume W_size == V_size
idx_range <- function(ngme_rep, name_or_idx) {
if (is.character(name_or_idx)) {
idx <- which(sapply(ngme_rep$models, function(model) model$name) == name_or_idx)
} else {
idx <- name_or_idx
}
models <- ngme_rep$models
tmp <- sapply(models, function(model) model$W_size)
sizes <- Reduce(`+`, tmp, accumulate = TRUE)
if (idx == 1) {
return(1:sizes[1])
} else {
return((sizes[idx - 1] + 1):sizes[idx])
}
}
#' @title posterior samples of different latent models
#' @description
#' Extract the posterior samples of different latent models
#'
#' @param ngme_object ngme object
#' @param model_name name of the model, or index of the model
#' @param type type of samples, "W" or "V"
#' @param replicate which replicate
#'
#' @return a data.frame of posterior samples (mesh_size * n_post_samples)
#' @export
ngme_post_samples <- function(
ngme_object,
model_name = 1,
type = "W",
replicate = 1) {
ngme_rep <- ngme_object$replicates[[replicate]]
stopifnot(
inherits(ngme_rep, "ngme_replicate"),
type %in% c("W", "V")
)
idx <- idx_range(ngme_rep, model_name)
if (type == "W") {
return(ngme_rep$post_W[idx, , ])
} else {
return(ngme_rep$post_V[idx, , ])
}
}
#' @title variance of the data or the latent field
#' @description
#' Compute the variance of the data or the latent field
#'
#' @param ngme_object ngme_model
#' @param model_name
#' if model_name = "data", then return the covariance matrix of the data (without measurement noise)
#' if the model_name is the name or index of the latent, then return the covariance matrix of the latent field
#' @param replicate which replicate (default = 1)
#'
#' @return a data.frame of posterior samples (mesh_size * n_post_samples)
#' @export
ngme_cov_matrix <- function(
ngme_object,
model_name = "data",
replicate = 1) {
ngme_rep <- ngme_object$replicates[[replicate]]
stopifnot(
"Please provide the correct ngme object." = inherits(ngme_object, "ngme"),
"Please provide the correct model name or index." =
model_name %in% c(
"data",
sapply(ngme_rep$models, function(model) model$name),
1:length(ngme_rep$models)
)
)
if (model_name == "data") {
V_mean <- apply(ngme_rep$post_V, 1, mean)
diag_K <- Matrix::bdiag(
sapply(ngme_rep$models, function(model) model$operator$K)
)
Q <- diag_K %*% diag(1 / V_mean) %*% Matrix::t(diag_K)
block_A <- Reduce(cbind, sapply(ngme_rep$models, function(model) model$A))
# return A Q^(-1) A^t,
return(block_A %*% Matrix::solve(Q, Matrix::t(block_A)))
} else {
K <- ngme_rep$models[[model_name]]$operator$K
idx <- idx_range(ngme_rep, model_name)
V_mean <- apply(ngme_rep$post_V[idx, , ], 1, mean)
Q <- K %*% diag(1 / V_mean) %*% Matrix::t(K)
return(solve(Q))
}
}
ngme_build_A <- function(model, mesh, map, operator, group, group_levels = NULL) {
group <- validate_rep_or_group(group, NULL)
if (is.null(group_levels)) group_levels <- levels(group)
if (inherits(mesh, "metric_graph")) {
A <- if (is.null(map)) NULL else mesh$fem_basis(map)
return(A)
}
if (model %in% c("tp", "spacetime")) {
mesh_t <- if (model == "tp") operator$first$mesh else operator$mesh[[1]]
mesh_s <- if (model == "tp") operator$second$mesh else operator$mesh[[2]]
stopifnot(
"Now only support first to be 1d model" = inherits(mesh_t, "inla.mesh.1d")
)
# watch-out! Should use as.factor to start at 1, not min(map[[1]])
# blk_group <- as.integer(as.factor(map[[1]]))
blk_group <- as.integer(map[[1]])
# important to start with 1
min_loc <- mesh_t$interval[[1]]
blk_group <- blk_group - min_loc + 1
stopifnot("The map[[1]] of the tp model is not valid, out of range." = all(blk_group <= (mesh_t$range[2] + 1)))
blk <- fmesher::fm_block(blk_group, n_block = mesh_t$n)
basis <- fmesher::fm_basis(mesh_s, loc = map[[2]])
A0 <- fmesher::fm_row_kron(Matrix::t(blk), basis)
if (model == "tp" && operator$second$model %in% c("bv", "bv_matern", "bv2")) {
# for tp-bv model
# check if group is valid
if (length(group) == 0) stop("Please provide the `group` argument.")
all(group %in% group_levels) || stop("The group is not valid.")
length(group) == length_map(map[[1]]) || stop("The length of group should be equal to the length of map.")
# tp bv model
# 1. expand A <- cbind(A, 0), double the column
# 2. move 2nd field to the 2nd half
# 3. re-order the 1st and 2nd
# e.g., (each field is of size 2)
# 1 2 3 4 5 6 7 8 to
# 1 2 5 6 3 4 7 8
A_expand <- cbind(
A0,
matrix(0, nrow = nrow(A0), ncol = ncol(A0))
)
# select row (of the 2nd field)
row_2nd_field <- group == group_levels[[2]]
half_1st <- 1:ncol(A0)
half_2nd <- 1:ncol(A0) + ncol(A0)
# Move the 2nd field to the right
A_expand[row_2nd_field, half_2nd] <-
A_expand[row_2nd_field, half_1st]
if (any(row_2nd_field)) {
A_expand[row_2nd_field, half_1st] <- 0
}
# Re-order to match K = K_t %x% K_bv (time-major, fields within time)
n_t <- operator$first$mesh$n
n_s <- operator$second$mesh$n
n_ts <- n_t * n_s
field1 <- matrix(seq_len(n_ts), nrow = n_s, ncol = n_t)
field2 <- field1 + n_ts
reorder_idx <- as.vector(rbind(field1, field2))
# return after re-order
return(ngme_as_sparse(A_expand[, reorder_idx]))
} else {
return(ngme_as_sparse(A0))
}
}
# bivariate model (including VAR(1))
if (model %in% c("bv", "bv2", "bv_matern", "var1")) {
# check if group is valid
if (length(group) == 0) stop("Please provide the `group` argument.")
all(group %in% group_levels) || stop("The group is not valid.")
length(group) == length_map(map) || stop("The length of group should be equal to the length of map.")
blk_group <- as.integer(as.factor(group))
blk <- fmesher::fm_block(blk_group, n_block = length(group_levels))
basis <- fmesher::fm_basis(mesh, loc = map)
A <- fmesher::fm_row_kron(Matrix::t(blk), basis)
return(ngme_as_sparse(A))
}
if (model == "re") {
return(ngme_as_sparse(operator$B_theta_K))
}
if (is.factor(map)) map <- as.integer(map) # for iid model
return(fmesher::fm_basis(mesh, loc = map))
}
contain_bv_model <- function(ngme) {
any(sapply(ngme$replicates[[1]]$models, function(model) {
model$model %in% c("bv", "bv2", "bv_matern")
})) ||
any(sapply(ngme$replicates[[1]]$models, function(model) {
model$model %in% c("tp") && model$operator$second$model %in% c("bv", "bv2", "bv_matern")
}))
}
#' @title get the trajectories of parameters of the model
#' @description
#' Get the trajectories of the parameters of the model
#'
#' @param ngme_object ngme object
#' @param model_name name of the model, if NULL, return the trajectories of the parameters of the measurement noise and fixed effects
#' @return a list of trajectories, each element is a matrix of trajectories (n_samples * n_trajectories)
#' @export
get_trajectories <- function(
ngme_object,
model_name) {
stopifnot(inherits(ngme_object, "ngme"))
ngme <- ngme_object$replicates[[1]]
if (!is.null(model_name) && model_name %in% names(ngme$models)) {
traj <- attr(ngme$models[[model_name]], "lat_traj")
ts <- get_latent_info(ngme$models[[model_name]])
} else {
traj <- attr(ngme, "block_traj")
ts <- get_noise_info(ngme$noise)
}
# str(traj)
# List of 4
# $ :'data.frame': 4 obs. of 1000 variables:
convert_to_3d_array <- function(traj) {
# Extract the dimensions
num_replicates <- length(traj) # Number of replicates
num_variables <- nrow(traj[[1]]) # Number of variables (4)
num_iterations <- ncol(traj[[1]]) # Number of iterations (1000)
# Initialize an empty 3D array
array_3d <- array(NA, dim = c(num_variables, num_iterations, num_replicates))
# Fill the array
for (replicate_index in seq_len(num_replicates)) {
array_3d[, , replicate_index] <- as.matrix(traj[[replicate_index]])
}
return(array_3d)
}
traj_3d <- convert_to_3d_array(traj)
ret <- list()
for (i in seq_along(ts$name)) {
name <- ts$name[i]
data <- traj_3d[i, , ]
ret[[name]] <- ts$trans[[i]](data)
}
return(ret)
}
#' @title get the parameters of the noise
#' @description
#' Get the parameters of the noise
#'
#' @param noise noise object
#' @param prefix prefix of the parameter
#' @return a list with parameter name as key and parameter value as value
#' @keywords internal
get_noise_param <- function(noise, prefix) {
B <- noise[[paste0("B_", prefix)]]
theta <- noise[[paste0("theta_", prefix)]]
if (length(theta) == 0) {
return(NULL)
}
if (is_stationary(B)) {
value <- if (prefix == "mu") theta else exp(theta)
} else {
value <- theta
}
name <- if (is_stationary(B)) prefix else paste0("theta_", prefix)
result <- list()
result[[name]] <- value
result
}
# extract parameters from all models in ngme object
extract_parameters <- function(ngme_object) {
stopifnot(inherits(ngme_object, "ngme"))
ngme_1rep <- ngme_object$replicates[[1]]
result_transformed <- list() # transformed parameters (user friendly)
result_raw <- list() # raw parameters (for optimizer)
# LATENT MODELS
models <- ngme_1rep$models
model_names <- names(models)
for (j in seq_along(models)) {
names <- models[[j]]$operator$param_name
trans <- models[[j]]$operator$param_trans
result_model_transformed <- list()
result_model_raw <- list()
for (i in seq_along(names)) {
result_model_transformed[[names[i]]] <- trans[[i]](models[[j]]$operator$theta_K[[i]])
if (is.function(trans[[i]]) && trans[[i]](3) == 3) {
raw_name <- names[i]
} else {
raw_name <- paste0("theta_", names[i])
}
result_model_raw[[raw_name]] <- models[[j]]$operator$theta_K[[i]]
}
# Use get_noise_param for noise parameters
noise <- models[[j]]$noise
noise_params_transformed <- list()
noise_params_raw <- list()
for (p in c("mu", "sigma", "nu")) {
noise_param <- get_noise_param(noise, p)
# special case for rw1 and rw2
if (models[[j]]$model %in% c("rw1", "rw2") && p == "sigma") {
noise_param$theta_sigma <- noise_param$theta_sigma[-1]
if (length(noise_param$theta_sigma) == 1) {
noise_param <- list(sigma = exp(noise_param$theta_sigma))
}
}
if (!is.null(noise_param)) {
noise_params_transformed <- c(noise_params_transformed, noise_param)
noise_params_raw <- c(noise_params_raw, noise_param)
}
}
result_model_transformed <- c(result_model_transformed, noise_params_transformed)
result_model_raw <- c(result_model_raw, noise_params_raw)
result_transformed[[model_names[j]]] <- result_model_transformed
result_raw[[model_names[j]]] <- result_model_raw
}
# FIXED EFFECTS and MEASUREMENT NOISE
beta <- ngme_1rep$feff
result_transformed$data$beta <- beta
result_raw$data$beta <- beta
m_noise <- ngme_1rep$noise
m_noise_params_transformed <- list()
m_noise_params_raw <- list()
for (p in c("mu", "sigma", "nu")) {
noise_param <- get_noise_param(m_noise, p)
if (!is.null(noise_param)) {
m_noise_params_transformed <- c(m_noise_params_transformed, noise_param)
m_noise_params_raw <- c(m_noise_params_raw, noise_param)
}
}
result_transformed$data <- c(result_transformed$data, m_noise_params_transformed)
result_raw$data <- c(result_raw$data, m_noise_params_raw)
list(transformed = result_transformed, raw = result_raw)
}
#' Extracts design matrix from a formula and data.
#'
#' This function takes a formula and a data frame, and returns a design matrix
#' based on the right-hand side of the formula. If the formula is `~.`, it treats
#' all columns in the data as the design matrix. Otherwise, it constructs a
#' design matrix without an intercept based on the specified formula.
#'
#' @param form A formula object, e.g., `~ x1 + x2` or `~.`.
#' @param data A data frame or matrix from which to extract the design matrix.
#' @return A numeric matrix representing the design matrix.
#' @export
#' @examples
#' data(mtcars)
#' # Extract a design matrix for 'mpg' and 'cyl'
#' X1 <- get_data_from_formula(~ mpg + cyl, mtcars)
#' # Extract a design matrix for all columns
#' X2 <- get_data_from_formula(~., mtcars)
#' # Extract a design matrix for 'hp'
#' X3 <- get_data_from_formula(~hp, mtcars)
get_data_from_formula <- function(form, data) {
form <- as.formula(form)
if (identical(form, ~.)) {
return(as.matrix(data))
}
rhs <- deparse(form[[2]])
form_no_intercept <- as.formula(paste("~ 0 +", rhs))
mf <- model.frame(form_no_intercept, data = data)
X <- model.matrix(form_no_intercept, data = mf)
return(X)
}
#' Check whether a newer stable version of ngme2 is available
#'
#' This function checks the package repository for the latest available ngme2
#' version. It does not install or update packages.
#'
#' @return Invisibly returns a list with the local version, remote version,
#' repository URL, and a logical \code{update_available} flag.
#' @export
ngme_update <- function() {
local_version <- utils::packageVersion("ngme2")
repos <- "https://davidbolin.github.io/ngme2/"
# Check available version in the repository
available <- tryCatch(
utils::available.packages(repos = repos),
error = function(e) NULL
)
if (is.null(available) || !"ngme2" %in% rownames(available)) {
message("Could not check for updates. Please check your internet connection or the repository URL.")
return(invisible(list(
local_version = as.character(local_version),
remote_version = NA_character_,
repository = repos,
update_available = NA
)))
}
remote_version <- available["ngme2", "Version"]
update_available <- utils::compareVersion(as.character(remote_version), as.character(local_version)) > 0
if (update_available) {
message(paste0("New stable version available: ", remote_version, " (local: ", local_version, ")"))
message("Install it manually from repository: ", repos)
} else {
message(paste0("ngme2 is up to date (version ", local_version, ")"))
}
invisible(list(
local_version = as.character(local_version),
remote_version = as.character(remote_version),
repository = repos,
update_available = update_available
))
}
#' Test OpenMP availability and report the number of threads.
#'
#' This function checks if OpenMP is available in the current R environment
#' and, if so, reports the number of OpenMP threads detected.
#' If OpenMP is not available, it reports a corresponding message.
#' It relies on an internal or external function `get_openmp_threads()`
#' which is assumed to return the number of threads or 0 if unavailable.
#'
#' @return Invisibly returns the detected number of OpenMP threads. A value of
#' zero means OpenMP is not available.
#' @export
openmp_test <- function() {
num_threads <- get_openmp_threads()
if (num_threads == 0) {
message("OpenMP not available.")
} else {
message("OpenMP is available, the default thread number is ", num_threads, ".")
}
invisible(num_threads)
}
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.