R/runSA.R

Defines functions compute_SA_variance bootstrap_SA validateSA plotSAtest Findbp testfittingSA sa2params saEndpoint runSA

Documented in bootstrap_SA Findbp plotSAtest runSA sa2params saEndpoint

#' Stochastic approximation for parameter fitting
#'
#' Estimate species interaction parameters and (optionally) environmental
#' preference parameters by stochastic approximation (SA). The function runs
#' multiple SA optimizations in parallel (when \code{threads > 1}) using
#' \pkg{foreach}.
#'
#' @param ocmat Numeric matrix (0/1). Rows are samples and columns are species.
#' @param enmat Optional numeric matrix. Environmental variables for each sample.
#'   Should be normalized/standardized beforehand.
#' @param rep Integer. Number of parallel runs (replicates) for optimization.
#' @param threads Integer. Number of cores used when running in parallel.
#' @param we Numeric. Weight decay parameter for AdamW.
#' @param totalit Integer. Total number of optimization iterations.
#' @param lambda Numeric. Sparsity penalty parameter (used when \code{sparse = TRUE}).
#' @param intv Integer. Interval of iterations at which intermediate results are recorded.
#' @param runadamW Logical. If \code{TRUE}, use AdamW-style optimization.
#' @param maxlr Numeric. Maximum learning rate.
#' @param sparse Logical. If \code{TRUE}, apply sparsity constraint.
#' @param getall Logical. If \code{TRUE}, return all replicate trajectories.
#' @param reporting Logical. If \code{TRUE}, print progress messages.
#'
#' @return If \code{getall = TRUE}, a list of SA trajectories (length \code{rep}).
#' Otherwise, a list with two elements, \code{c(params, enmat)} is returned.
#' \code{params} is the concatenated matrix of the estimated parameter set:
#' \describe{
#'   \item{h}{Implicit environmental effects on species occurrence}
#'   \item{J}{Species-species interaction matrix}
#'   \item{g}{(If \code{enmat} is not \code{NULL}) Explicit effects of the environmental factors.}
#' }
#'
#'
#' @useDynLib ELAplus, .registration=TRUE
#' @importFrom Rcpp sourceCpp
#' @import foreach
#' @export
runSA <- function(ocmat, enmat=NULL,  rep=128, threads=1, we=0.001, 
                  totalit=1000, lambda=0.01,intv=100, runadamW=TRUE,
                  maxlr = 0.005, sparse=TRUE, getall=FALSE, reporting=TRUE){
    #if(!is.matrix(ocmat)){ocmat <- as.matrix(ocmat)}
    if(!sparse){lambda <- 0}
  
    #if(!runadamW){we <- 0}
  
    if (threads > 1) {
      cluster <- parallel::makeCluster(threads, type = if (.Platform$OS.type == "windows") "PSOCK" else "FORK")
      parallel::clusterCall(cluster, function(x) .libPaths(x), .libPaths())
      parallel::clusterSetRNGStream(cluster)
      doParallel::registerDoParallel(cluster)
      on.exit(parallel::stopCluster(cluster), add = TRUE)
    }

    if(is.null(enmat)){

    	## ============================================== ##
        ## -- without explicit variables

        if(reporting){cat('Start parameter fitting:\n')}
        s <- proc.time()[3]
        if(threads>1){fittingMat <- foreach(i = 1:rep, .packages="ELAplus") %dopar% {
            SAres <- SA(ocData = ocmat, we = we, totalit = totalit, 
                        runadamW = runadamW, sparse = sparse,
                        lambda = lambda, intv = intv, maxlearningrate = maxlr)
            return(SAres)
        }}else{fittingMat <- foreach(i = 1:rep) %do% {
	      if(reporting){cat('.')}
            SAres <- SA(ocData = ocmat, we = we, totalit = totalit, 
                        runadamW = runadamW, sparse = sparse,
                        lambda = lambda, intv = intv,maxlearningrate = maxlr)
            return(SAres)
        }}

        if(reporting){cat(sprintf('SA: elapsed time %.2f sec\n\n', proc.time()[3]-s))}
        ## ============================================== ##
      
        if(getall){
          return(fittingMat)
        }else{
            return(list(saEndpoint(fittingMat,ocmat),enmat))}
          
	    }else{

        ## ============================================== ##
        ## -- with explicit variables
	      #if(!is.matrix(enmat)){enmat <- as.matrix(enmat)}

        if(reporting){cat('Start parameter fitting\n')}
        s <- proc.time()[3]
        if(threads>1){fittingMat <- foreach(i = 1:rep, .packages="ELAplus") %dopar% {
            SAres <- fullSA(ocData = ocmat, envData=as.matrix(enmat), 
                            runadamW = runadamW, sparse = sparse, we = we, 
                            totalit = totalit, lambda = lambda, intv = intv,
                            maxlearningrate = maxlr)
            return(SAres)
        }}else{fittingMat <- foreach(i = 1:rep) %do% {
            if(reporting){cat('.')}
            SAres <- fullSA(ocData = ocmat, envData=as.matrix(enmat),
                            runadamW = runadamW, sparse = sparse, we = we, 
                            totalit = totalit, lambda = lambda, intv = intv,
                            maxlearningrate = maxlr)
            return(SAres)
        }}

        if(reporting){cat(sprintf('SA: elapsed time %.2f sec\n\n', proc.time()[3]-s))}
        if(getall){
          return(fittingMat)
        }else{
          return(list(saEndpoint(fittingMat,ocmat,enmat=enmat),enmat))}
        ## ============================================== ##
	
    }
}


#' Extract endpoint parameters from \link{runSA} output
#'
#' Summarize the final parameter estimates across SA replicates by averaging
#' the last recorded block of parameters.
#'
#' @param fittingMat A list of SA trajectories returned by \code{runSA(getall = TRUE)}.
#' @param ocmat Numeric matrix (0/1). Rows are samples and columns are species.
#' @param enmat Optional environmental parameter matrix used in fitting.
#'
#' @return A numeric matrix of median endpoint parameters across serials. 
#' Rownames correspond to samples and Colnames correspond to species list
#'
#' @export
saEndpoint <- function(fittingMat,ocmat,enmat=NULL){
  
  num_cols <- ncol(ocmat)
  packed_results_end <- lapply(fittingMat, 
                               function(x) x[(nrow(x) - num_cols + 1):nrow(x),])
  params_all <- lapply(packed_results_end, function(x) x[, 1:(ncol(x) - 2)])
  #upd <- lapply(sa, function(x) x[, (ncol(x) - 1):ncol(x)]) 
  
  # median
  if (length(params_all) == 1) {
    params_mat <- params_all[[1]]
  } else {
    arr <- simplify2array(params_all)
    params_mat <- apply(arr, c(1, 2), median, na.rm = TRUE)
  }
  
  # mean
  #params_mat <- params_all[[1]]
  #if (length(params_all)>1){
  #  for (n in 2:length(params_all)) {
  #    params_mat <- params_mat + params_all[[n]]
  #  }
  #
  #  params_mat <- params_mat/length(params_all)
  #}
  rownames(params_mat) <- colnames(ocmat)
  
  if(is.null(enmat)){
    colnames(params_mat) <- c("h",paste('J',colnames(ocmat),sep='.'))
  }
  else{
    encols <- ncol(enmat)
    
    if( ncol(as.matrix(enmat))==1 ){
      gname <- paste('g', colnames(enmat), sep='.')
      ncols <- ncol(ocmat)+2
    }else{
      gname <- paste('g',1:ncol(enmat), sep='.')
      ncols <- ncol(ocmat)+ncol(enmat)+1
    }
    
    colnames(params_mat) <- c('h', gname, paste('J',colnames(ocmat),sep='.'))
  }
  
  return(params_mat)
}


#' Convert SA output to model parameter objects
#'
#' Convert the output of \link{runSA} / \link{saEndpoint} into parameter
#' objects in (extended) maximum pairwise entropy model:
#' \code{h}, \code{J}, and optionally \code{g} when environmental parameter
#' matrix is specified.
#'
#' @param sa A list in the form \code{list(param_matrix, enmat)} where
#'   \code{param_matrix} contains estimated parameters.
#' @param env Optional numeric vector of environmental values (column names correspond
#'    to environmental factors).Default is NULL.
#'
#' @return A list with elements \code{he} (h), \code{je} (J), \code{ge} (g or NULL),
#'   and \code{hge} (combined field).
#'
#'
#' @useDynLib ELAplus, .registration=TRUE
#' @export
sa2params <- function(sa, env=NULL){
  sadim <- dim(sa[[1]])
  na <- rownames(sa[[1]])
  if(sadim[2] > sadim[1]+1){
      if(length(env)==0){env <- rep(0, sadim[2]-sadim[1]-1)} ## !!!
        he <- `names<-`(sa[[1]][, 1], na)
        je <- `dimnames<-`(sa[[1]][, ((sadim[2] - sadim[1]) + 1):sadim[2]],list(na, na))
        ge <- `dimnames<-`(as.matrix(sa[[1]][, 2:(sadim[2] - sadim[1])]),list(na, colnames(sa[[2]])))
        hge <- (he + ge %*% env)[,1]
        #hge <- t(t(envecs %*% t(ge)) + he)
  }else{
        he <- sa[[1]][, 1] %>% 'names<-'(na)
        je <- sa[[1]][, -1] %>% 'dimnames<-'(list(na,na))
        ge <- NULL
        hge <- he
  }
  return(list(he, je, ge, hge))}


#'testfittingSA: testrun of SA across given parameter sets using subset of ocmatrix
#'
#' Evaluate SA trajectories by validating the predicted occurrence matrix under
#' estimated parameters at each recorded time point.
#'
#' @param ocmat Numeric matrix (0/1). Observed community matrix.
#' @param sa_results List of SA parameter trajectories (e.g., from \code{runSA(getall=TRUE)}).
#' @param rep Integer. Number of replicates.
#' @param intv Integer. Recording interval used in fitting.
#' @param enmat Optional environmental matrix.
#'
#' @return A numeric matrix of validation scores (rows = replicates, columns = time points).
#'
#' @keywords internal
#' @noRd
testfittingSA <- function(ocmat,sa_results,rep,intv,enmat=NULL){
  nspecies = ncol(ocmat)
  num_timepoints <- nrow(sa_results[[1]]) / nspecies - 1
  timepoints <- sapply(0:num_timepoints, function(x) (x + 1) * intv)
  serials <- 1:rep
  res_serials <- matrix(0, ncol= (num_timepoints+1), nrow=rep,
                       dimnames=list(serials,timepoints))
  for(i in 1:length(sa_results)){
    rowidx <- 1
    timeidx <- 1
    SAparams <- sa_results[[i]]
    while(rowidx < nrow(SAparams)){
      
      params<-sa2params(list(SAparams[rowidx:(rowidx+nspecies-1),
                                                  1:(ncol(SAparams)-2)],enmat))
      res_serials[i,timeidx] <- validateSA(ocmat,params[[1]],params[[2]],
                                           g=params[[3]],enmat=enmat)
      rowidx <- rowidx + nspecies
      timeidx <- timeidx + 1
    }
  }
  return(res_serials)
}

#' Search best hyperparameters for SA fitting (lambda and weight decay) by cross-validation
#'
#' Repeating cross-validation over a grid of hyperparameters for
#' simulated annealing-based fitting, evaluates prediction/fitting errors on
#' held-out samples, and returns candidate best parameter/timepoint combinations.
#'
#' @param ocmat Numeric matrix (0/1). occurrence matrix of the communities.
#' @param enmat Optional environmental parameter matrix.
#' @param cvmode Cross-validation mode. One of `"10fold"`, `"5fold"`, or `"loo"`.
#'   `"loo"` performs leave-one-out cross-validation. Default is `"10fold"`.
#' @param nrepeat Number of repeated cross-validation runs for k-fold CV.
#'   Ignored when `cvmode = "loo"`. Default is `10`.
#' @param we Numeric vector. Candidate weight decay values.
#' @param totalit Integer. Total iterations for SA.
#' @param lmd Numeric vector. Candidate \code{lambda} values.
#' @param maxlrs Numeric vector. Maximum learning rate.
#' @param rep Integer. Number of replicates for \link{runSA}.
#' @param intv Integer. Recording interval.
#' @param threads Integer. Number of cores.
#' @param tol Numeric. Error tolerance for finding the best hyperparameters (default: 0.03)
#' @param runadamW Logical. Use AdamW.
#' @param sparse Logical. Use sparsity penalty.
#' @param reporting Logical. Print progress.
#' @param fastfitting Logical. If \code{TRUE}, returns early iterations within the range
#' specified by the parameter \code{tol} (default: 0.03) of best error.
#' @importFrom utils head
#'
#' @return A list with:
#' \describe{
#'   \item{best_candidates}{A named list of best hyperparameter settings and scores.}
#'   \item{allresults}{All validation results for each grid point.}
#' }
#'
#' @export

Findbp <- function(ocmat,enmat=NULL,cvmode=c("10fold","5fold","loo"),nrepeat=5,
                   we=c(0.001),maxlrs = c(0.005),
                   totalit=4000,lmd=c(0.0005,0.001,0.0025,0.005,0.0075),
                   rep=8,intv=100,threads=1,tol=0.03,
                   runadamW=TRUE,sparse=TRUE,fastfitting=TRUE,reporting=TRUE){
  min_points <- list()
  n_samples <- nrow(ocmat)
  if(n_samples < 2){
    stop("At least two samples are required.")
  }
  cvmode <- match.arg(cvmode)
  nrepeat <- as.integer(nrepeat)
  if(nrepeat < 1){
    stop("nrepeat must be >= 1.")
  }
  if(!runadamW){we <- c(0)}
  if(!sparse){lmd <- c(0)}
  ## ------------------------------------------------------------
  ## Make repeated cross-validation folds
  ## ------------------------------------------------------------
  split_list <- list()
  if(cvmode == "loo"){
    ## Leave-one-out is deterministic; nrepeat is ignored.
    for(i in seq_len(n_samples)){
      split_list[[length(split_list) + 1]] <- list(
        repeat_id = 1,
        fold_id = i,
        label = paste0("loo_", i),
        train = setdiff(seq_len(n_samples), i),
        test  = i
      )
    }
    split_prefix <- "loo"
  }else{
    k <- ifelse(cvmode == "10fold", 10, 5)
    if(n_samples < k){
      stop("Number of samples must be >= number of folds.")
    }
    for(r in seq_len(nrepeat)){
      ## Randomly assign samples to approximately equal-sized folds
      fold_id <- sample(rep(seq_len(k), length.out=n_samples))
      for(f in seq_len(k)){
        test_idx <- which(fold_id == f)
        train_idx <- setdiff(seq_len(n_samples), test_idx)
        split_list[[length(split_list) + 1]] <- list(
          repeat_id = r,
          fold_id = f,
          label = paste0("rep", r, "_fold", f),
          train = train_idx,
          test  = test_idx
        )
      }
    }
    split_prefix <- paste0(k, "fold")
  }
  n_splits <- length(split_list)
  ## ------------------------------------------------------------
  ## Make task grid:
  ## one task = one hyperparameter setting x one repeat/fold split
  ## ------------------------------------------------------------
  task_grid <- expand.grid(
    lambda = lmd,
    we = we,
    maxlr = maxlrs,
    split_id = seq_len(n_splits),
    stringsAsFactors = FALSE
  )
  n_tasks <- nrow(task_grid)
  ## ------------------------------------------------------------
  ## Required packages
  ## ------------------------------------------------------------
  #if(!requireNamespace("foreach", quietly=TRUE)){
  #  stop("Package 'foreach' is required.")
  #}
  #if(!requireNamespace("magrittr", quietly=TRUE)){
  #  stop("Package 'magrittr' is required.")
  #}
  #suppressPackageStartupMessages({
  #  library(foreach)
  #  library(magrittr)
  #})
  ## ------------------------------------------------------------
  ## Parallel setup
  ## ------------------------------------------------------------
  use_parallel <- 1
  if(use_parallel){
    if(!requireNamespace("doParallel", quietly=TRUE)){
      stop("Package 'doParallel' is required when threads > 1.")
    }
    n_workers <- min(threads, n_tasks)
    cluster <- parallel::makeCluster(n_workers)
    parallel::clusterCall(cluster, function(x) .libPaths(x), .libPaths())
    #parallel::clusterEvalQ(cluster, {
    #  suppressPackageStartupMessages({
    #    library(foreach)
    #    library(magrittr)
    #    library(ELAplus)
    #  })
    #  NULL
    #})
    doParallel::registerDoParallel(cluster)
    on.exit(parallel::stopCluster(cluster), add=TRUE)
  }
  ## ------------------------------------------------------------
  ## Run all tasks
  ## ------------------------------------------------------------
  run_one_task <- function(i){
    l <- task_grid$lambda[i]
    w <- task_grid$we[i]
    maxlr <- task_grid$maxlr[i]
    split_id <- task_grid$split_id[i]
    split <- split_list[[split_id]]
    train_idx <- split$train
    test_idx  <- split$test
    octraining <- ocmat[train_idx,,drop=FALSE]
    octest     <- ocmat[test_idx,,drop=FALSE]
    if(!is.null(enmat)){
      envtraining <- enmat[train_idx,,drop=FALSE]
      envtest     <- enmat[test_idx,,drop=FALSE]
    }else{
      envtraining <- NULL
      envtest     <- NULL
    }
    sa_results <- runSA(octraining, enmat=envtraining,
                        rep=rep, threads=1, we=w,maxlr=maxlr,
                        totalit=totalit, lambda=l, intv=intv,
                        runadamW=runadamW, sparse=sparse,
                        getall=TRUE, reporting=FALSE)
    current_results <- testfittingSA(octest, sa_results,
                                     rep=rep, intv=intv,
                                     enmat=envtest)
    ## Average across SA replicates within this split
    error_vec <- colMeans(current_results, na.rm=TRUE)
    list(
      lambda = l,
      we = w,
      maxlr = maxlr,
      split_id = split_id,
      split_label = split$label,
      repeat_id = split$repeat_id,
      fold_id = split$fold_id,
      error = error_vec
    )
  }
  if(reporting){cat('Start hyper-parameter search:\n')}
  if(reporting){cat(sprintf("There are %d tasks\n", n_tasks))}
  
  s <- proc.time()[3]
  
  task_results <- if(use_parallel){
    foreach(
      i = seq_len(n_tasks),
      .packages = c("foreach", "magrittr", "ELAplus"),
      .export = c("testfittingSA", "validateSA", "sa2params")
    ) %dopar% {
      run_one_task(i)
    }
  }else{
    foreach(
      i = seq_len(n_tasks),
      .packages = c("foreach", "magrittr", "ELAplus"),
      .export = c("testfittingSA", "validateSA", "sa2params")
    ) %do% {
      run_one_task(i)
    }
  }
  
  ## ------------------------------------------------------------
  ## Reconstruct allresults in the same format:
  ## allresults[[lambda_we]] = matrix of CV errors
  ## rows = repeat/fold splits, columns = timepoints
  ## ------------------------------------------------------------
  allresults <- list()
  min_points <- list()
  combo_names <- unique(paste(task_grid$lambda, task_grid$we, task_grid$maxlr, sep="_"))
  for(combo_name in combo_names){
    idx <- which(paste(task_grid$lambda, task_grid$we, task_grid$maxlr, sep="_") == combo_name)
    combo_results <- task_results[idx]
    current_results <- do.call(
      rbind,
      lapply(combo_results, function(x) x$error)
    )
    rownames(current_results) <- sapply(combo_results, function(x) x$split_label)
    allresults[[combo_name]] <- current_results
    mean_results <- apply(current_results, 2, mean, na.rm=TRUE)
    combo_parts <- strsplit(combo_name, "_", fixed=TRUE)[[1]]
    l <- combo_parts[1]
    w <- combo_parts[2]
    maxlr <- combo_parts[3]
    for (m in names(mean_results)){
      min_points[[paste(l, w, maxlr, as.integer(m), sep = "_")]] <- as.numeric(mean_results[m])
    }
  }
  if(reporting){cat(sprintf("Finished %d tasks\n", n_tasks))}
  if(reporting){cat(sprintf('SA: elapsed time %.2f sec\n\n', proc.time()[3]-s))}
  ## ------------------------------------------------------------
  ## Select best parameter/timepoint combinations
  ## ------------------------------------------------------------
  if(fastfitting){
    min_err <- min(unlist(min_points))
    min_points <- min_points[min_points < min_err*(1+tol)]
    sorted_min_points <- min_points[order(sapply(names(min_points),
                                                 function(x) {as.numeric(unlist(strsplit(x,split="_"))[[4]])}))]
  }else{
    sorted_min_points <- min_points[order(sapply(min_points, function(x) x[1]))]
  }
  return(list(head(sorted_min_points,min(length(sorted_min_points),5)),allresults))
}

#'plotSAtest: plot the results of SA fitting (Findbp output)
#' 
#' Plot mean validation scores over time for the results returned by
#' \link{Findbp}, including \code{95\%} confidence intervals.
#'
#' @param sa_results Output list from \link{Findbp}.
#' @param ylim Numeric length-2 vector (ymin, ymax). Default: \code{c("auto","auto")}.
#'
#' @return A \pkg{ggplot2} object.
#' @importFrom tidyr gather
#' @importFrom ggplot2 geom_line geom_ribbon scale_y_continuous
#' @details
#' This function uses \pkg{ggplot2} and tidyverse-style data manipulation.
#' Ensure required packages are installed.
#'@export
plotSAtest <- function(sa_results,ylim=c("auto","auto")){
  
  if(is.null(names(sa_results))){
    names(sa_results) <- paste0("set_", seq_along(sa_results))
  }
  
  summary_df <- data.frame()
  
  for(i in seq_along(sa_results)){
    tmp <- as.data.frame(sa_results[[i]])
    tmp$rowname <- rownames(tmp)
    rownames(tmp) <- NULL
    res_serials_melt <- tidyr::pivot_longer(
      tmp,
      cols = setdiff(names(tmp), "rowname"),
      names_to = "timepoint",
      values_to = "value"
    )
    
    res_serials_melt$timepoint <- as.numeric(res_serials_melt$timepoint)
    results_i <- res_serials_melt
    results_i$id <- names(sa_results)[i]
    summary_df <- rbind(summary_df,results_i)
  }
  
  summary_mean <- summary_df %>%
    dplyr::group_by(.data$id,.data$timepoint) %>%
    dplyr::summarise(n=sum(!is.na(.data$value)),
                     mean=mean(.data$value, na.rm=TRUE),
                     sd=stats::sd(.data$value, na.rm=TRUE),
                     .groups="drop") %>%
    dplyr::mutate(sem=ifelse(.data$n > 1, .data$sd / sqrt(.data$n), 0),
                  tcrit=ifelse(.data$n > 1, stats::qt(0.975, .data$n - 1), 0),
                  CI_lower=.data$mean - .data$tcrit * .data$sem,
                  CI_upper=.data$mean + .data$tcrit * .data$sem)
  
  if(ylim[[1]]=="auto"){
    ymin <- min(summary_mean$CI_lower, na.rm=TRUE)
  }else{
    ymin <- as.numeric(ylim[[1]])
  }
  
  if(ylim[[2]]=="auto"){
    ymax <- max(summary_mean$CI_upper, na.rm=TRUE)
  }else{
    ymax <- as.numeric(ylim[[2]])
  }
  
  ggplot(summary_mean, aes(x=.data$timepoint, y=.data$mean, color=.data$id)) +
    geom_line(aes(color=.data$id)) +
    geom_ribbon(aes(ymin=.data$CI_lower, ymax=.data$CI_upper, fill=.data$id),
                color="grey70", alpha=0.4) +
    scale_y_continuous(limits=c(ymin,ymax))
}


#'validateSA: to check the predictability of SA results by computing ocmatrix using
#'the estimated parameter set.
#'
#' @param ocmat Numeric matrix (0/1). Occurrence matrix.
#' @param h Numeric vector. Implicit environmental effects on species occurrence.
#' @param j Numeric matrix. Species-interaction matrix.
#' @param g Optional numeric matrix. Explicit environmental effects on species occurrence.
#' @param enmat Optional environmental parameters matrix.
#'
#' @return A numeric score (negative mean log of prediction error).
#'
#' @noRd
validateSA <- function(ocmat,h,j,g=NULL,enmat=NULL){
  if(is.null(enmat)){
    pp <- abs(1 - ocmat - 1 / (1 + exp(t(-h - t(ocmat %*% j)))))
  }
  else{
    pp <- abs(1 - ocmat - 1 / (1 + exp(-t(t(enmat %*% t(g)) + h) - ocmat %*% j)))
    #pp <- abs(1 - ocmat - 1 / (1 + exp(t(t(-hge - (ocmat %*% J))))))
  }
  return( -mean(log(pp[pp > 0])))
}

#' Parameter fitting with bootstrap sampling of datasets
#'
#' Perform \link{runSA} algorithm and estimate parameters using bootstrap resampled
#' data set (\code{ocmat} and \code{enmat}). 
#' The basic configuration is same as \link{runSA}.
#'
#' @param ocmat Numeric matrix (0/1). Rows are samples and columns are species.
#' @param enmat Optional numeric matrix. Environmental variables for each sample.
#'   Should be normalized/standardized beforehand.
#' @param rep Integer. Number of parallel runs (replicates) for optimization.
#' @param threads Integer. Number of cores used when running in parallel.
#' @param we Numeric. Weight decay parameter for AdamW.
#' @param totalit Integer. Total number of optimization iterations.
#' @param lambda Numeric. Sparsity penalty parameter (used when \code{sparse = TRUE}).
#' @param runadamW Logical. If \code{TRUE}, use AdamW-style optimization.
#' @param maxlr Numeric. Maximum learning rate.
#' @param sparse Logical. If \code{TRUE}, apply sparsity constraint.
#' @param bootstrap.it number of bootstrap trials (default: 1000).
#' @param reporting Logical. If \code{TRUE}, print progress messages.
#'
#' @return a list of SA parameters estimated with bootstrap resampling (length \code{bootstrap.it}).
#'
#' @useDynLib ELAplus, .registration=TRUE
#' @importFrom Rcpp sourceCpp
#' @import foreach
#' @export
bootstrap_SA <- function(ocmat, enmat=NULL, bootstrap.it=2000, rep=128, threads=1, we=0.001, 
                         totalit=1000, lambda=0.01, runadamW=TRUE,maxlr = 0.005, sparse=TRUE, reporting=TRUE){
  if(reporting){cat('Start parameter fitting:\n')}
  s <- proc.time()[3]
  #if(!is.matrix(ocmat)){ocmat <- as.matrix(ocmat)}
  if(!sparse){lambda <- 0}
  
  #if(!runadamW){we <- 0}
  
  bootstrap_res <- vector("list", bootstrap.it)
  n_samples <- nrow(ocmat)
  
  for (i in 1:bootstrap.it){
    # bootstrap sampling
    if(reporting){
      if(stats::runif(1) > max((bootstrap.it - 100)/bootstrap.it, 0))
      cat('.')
      }
    randidxs <- sample(n_samples, size = n_samples, replace = TRUE)
    ocrand <- ocmat[randidxs, , drop = FALSE]
    
    if(!is.null(enmat)){
      envrand <- enmat[randidxs, , drop = FALSE]
    }
    else{envrand=NULL}
    
    sa_results <- runSA(ocrand, enmat=envrand, rep=rep, threads=threads, we=we, 
                        totalit=totalit, lambda=lambda,intv=100, runadamW=runadamW, 
                        sparse=sparse,maxlr=maxlr,getall=FALSE, reporting=FALSE)
    
    bootstrap_res[[i]] <- sa_results[[1]]
  }
  if(reporting){cat(sprintf('\nSA: elapsed time %.2f sec\n\n', proc.time()[3]-s))}
  return(bootstrap_res)
}

#' Check variability in the stochastic approximation algorithm. 
#'
#' Perform bootstrap resampling of the estimated paramters using \link{runSA} with \code{getall=TRUE}.
#' This bootstrap sampling reflects the intrinsic variability of the stochastic approximation algorithm 
#' and does not correspond to standard confidence intervals based on resampled data.
#' Use \link{bootstrap_SA} to assess finite-sample estimation error. 
#'
#' @param fittingMat output of runSA function (need all run results in serials) 
#' @param ocmat Numeric matrix (0/1). Rows are samples and columns are species.
#' @param enmat Optional numeric matrix. Environmental variables for each sample.
#'   Should be normalized/standardized beforehand.
#' @param resamplesize total resampling size (default: length(fittingMat)), necessary when \code{type="quantile"}.
#' @param iterations number of bootstrap iterations (default: 1000).
#'
#' @return a list of SA parameters estimated with bootstrap resampling (length \code{iterations}).
#' @noRd
compute_SA_variance <- function(fittingMat,ocmat,enmat=NULL,resamplesize=NULL,iterations = 1000){
  if(is.null(resamplesize)){resamplesize<-length(fittingMat)}
  rand_sample_idx <- lapply(1:iterations, function(i) {sample(1:length(fittingMat),size=resamplesize,replace=TRUE)})
  rand_sa_params <- vector("list", iterations)
  for (i in 1:iterations){
    rand_serial <- fittingMat[rand_sample_idx[[i]]]
    rand_sa_params[[i]] <- saEndpoint(rand_serial,ocmat,enmat=enmat)
  }
  return(rand_sa_params)
}

Try the ELAplus package in your browser

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

ELAplus documentation built on May 28, 2026, 5:09 p.m.