Nothing
#' @title
#' Predictive Modeling Engine
#'
#' @description
#' Modeling of spatially varying phenomena based on landscape similarity to
#' stratification units. If each stratification unit across geographic space
#' represents a distinct landscape configuration (in terms of multiple landscape
#' factors and/or factor scales), and if each landscape configuration influences
#' a phenomenon in a distinct way, then the spatial variability of that
#' phenomenon can be assessed across a landscape by relating each geographic
#' location to each distinct landscape configuration. Therefore, the more
#' similar a geographic location is to the landscape configuration represented
#' by a given stratification unit, then also the more similar the response of a
#' phenomenon will be at that location to the typical response for conditions
#' within the given stratification unit. Both continuous and categorical
#' response variables are supported. For categorical responses, each category
#' must be identified by an integer value.
#'
#' @param res.type Character. Type of response to model. Options are "cont" for
#' continuous, and "cat" for categorical response. Default: "cont"
#' @param ls.rast SpatRaster, as in \code{\link[terra]{rast}}. Multi-layer
#' SpatRaster representing landscape similarities to stratification units.
#' Only similarities for units with a representative observation are allowed.
#' Character prefix in the file name of similarities is allowed.
#' @param n.win Integer. Positive number indicating how many winning
#' stratification units should be considered. See \strong{Details}. Default: 3
#' @param su.repobs Data frame. The first column of this data frame must contain
#' only the numeric code for the stratification units (without prefix). Each
#' additional column must contain the value of the representative response
#' observation for each stratification unit. Multiple response variables are
#' allowed (one per column). Note that all response variables in the data
#' frame must share the same type (\emph{res.type}). See example on issues
#' related to \strong{non-explicit column names}.
#' @param tiles SpatVector, as in \code{\link[terra]{vect}}. Spatial vector of
#' polygon geometry with the boundaries of the area of interest. This vector
#' can be subdivided in regions (i.e., tiles) to balance memory allocation and
#' processing speed (see \strong{Details}). If this vector is tiled, then its
#' attribute table must only contain an ID column with a unique identifier for
#' each tile (1,2,...,n). Additionally, This vector must have the same
#' coordinate reference system as \emph{ls.rast}.
#' @param parallel Boolean. Perform parallel processing? A parallel backend
#' needs to be registered beforehand with
#' \code{\link[doParallel]{registerDoParallel}}. Moreover, a tiled spatial
#' vector should be supplied for \emph{tiles}. Default: FALSE
#' @param outdir Character. String specifying the path for the output raster
#' tiles/layer(s) of modeled response(s). Default: "."
#' @param tile.rm Boolean. Should the tiles of modeled response(s) be removed
#' from disk after the tile merging process? Default: TRUE
#' @param extension Character. String specifying the extension for the output
#' raster layer(s) of modeled response(s). Default: ".tif"
#' @param verbose Boolean. Show warning messages in the console? Default: FALSE
#' @param ... Additional arguments as for \code{\link[terra]{writeRaster}}.
#'
#' @return
#' Multi-layer or single-layer SpatRaster with modeled response(s).
#'
#' @details
#' The predictive modeling process is cell-wise, which means that it operates on
#' a cell-by-cell basis. For a given cell occurring in the geographic space
#' supported by a raster layer, the predictive modeling engine first identifies
#' the \emph{n} stratification units to which the given cell is most similar
#' (i.e., 'winning stratification units'). The engine is able to identify the
#' winning stratification units thanks to the user-provided set of landscape
#' similarity layers \emph{ls.rast}. Subsequently, the response value from the
#' representative observation for each winning stratification unit is
#' identified. In the case of a continuous response, a weighted average of
#' representative response values is performed. For each representative response
#' value, the weight is proportional to the corresponding stratification unit's
#' landscape similarity value in the given cell. The result of the weighted
#' average is assigned as the response value in the given cell. In the case of a
#' categorical response, the modal value from the representative response values
#' of the \emph{n} winning stratification units is assigned to the given cell.
#'
#' Note that the name for each raster layer in \emph{ls.rast} should match the
#' numeric code of the corresponding stratification unit, which is obtained from
#' the column of numeric codes in \emph{su.repobs}. Nevertheless, raster layer
#' names in \emph{ls.rast} with a character prefix in the numeric code and/or
#' file extension should work fine (e.g., "su_1101.tif" instead of "1101"). If
#' the landscape similarity layers in \emph{ls.rast} were created with
#' \code{\link{similarity}}, then raster layer names will not have any prefix
#' nor extension as part of the numeric code.
#'
#' When dealing with large geographic spaces, high raster resolutions (i.e.,
#' small cell sizes), or both, a considerable amount of memory is required to
#' perform the modeling process. To reduce memory usage, the predictive modeling
#' engine performs tile-based processing of landscape similarity layers and
#' \strong{writes results directly on disk}. Tile-based processing increases the
#' computational time, thus parallelization is allowed by setting up a parallel
#' backend. If parallelization is enabled, then care must be taken with the size
#' of the tiles since larger sizes will have a greater impact on memory usage.
#' Consequently, the parallel, tile-based processing will be less useful.
#'
#' @examples
#' require(terra)
#' p <- system.file("exdat", package = "rassta")
#' # Multi-layer SpatRaster of landscape similarities
#' fls <- list.files(path = p, pattern = "su_", full.names = TRUE)
#' ls <- terra::rast(fls)
#' # Numeric code and representative response value for stratification units
#' fro <-list.files(path = p, pattern = "repobs.csv", full.names = TRUE)
#' ro <- read.csv(fro)
#' # Extract only those stratification units with representative value
#' ls <- ls[[as.character(ro$SU)]]
#' # SpatVector with processing tiles
#' fti <- list.files(path = p, pattern = "tiles.shp", full.names = TRUE)
#' ti <- terra::vect(fti)
#' # Directory for temporary files
#' o <- tempdir()
#' # Perform predictive modeling of continuous response
#' r <- engine(res.type = "cont", ls.rast = ls, n.win = 2, su.repobs = ro,
#' tiles = ti, outdir = o, overwrite = TRUE
#' )
#' # Plot modeled response
#' if(interactive()){plot(r)}
#' # Clean temporary files
#' file.remove(list.files(path = o, pattern = "soc.tif", full.names = TRUE))
#' #
#' #-------
#' # A note on non-explicit response's names (obtained from su.repobs):
#'
#' ## This will result in incorrectly modeled response values
#' x <- c("SOM", "SOM_30cm", "SOM_45cm") # SOM = soil organic matter
#' grep(x[1], x) # Non explicit
#' grep(x[2], x) # Explicit
#' grep(x[3], x) # Explicit
#'
#' ## This will result in correct values
#' x <- c("SOM_15cm", "SOM_30cm", "SOM_45cm")
#' grep(x[1], x) # Explicit
#' grep(x[2], x) # Explicit
#' grep(x[3], x) # Explicit
#'
#' @export
#' @family
#' Functions for Predictive Modeling
#' @rdname
#' engine
#' @seealso
#' \code{\link{similarity}}, \code{\link{observation}}
#'
engine <- function(res.type = "cont", ls.rast, n.win = 3, su.repobs, tiles,
parallel = FALSE, outdir = ".", tile.rm = TRUE,
extension = ".tif", verbose = FALSE, ...)
{
#-----Binding variables to prevent devtools::check() notes-----#
i <- j <- k <- l <- m <- NULL
#--------------------------------------------------------------#
# Define processing scheme (disk [parallel] VS memory [sequential])
`%ps%` <- if(parallel == TRUE) { foreach::`%dopar%` } else { foreach::`%do%` }
# According to response type (continuous or categorical)
if (res.type == "cont") {
# Rewrite name of id field for tiles
base::names(tiles) <- "id"
# Detect number of responses to infer
n.resp <- base::ncol(su.repobs) - 1 # minus id column
# Loop for tiles
tname <- foreach::foreach(i = 1:base::length(tiles)) %ps% {
# Landscape similarity matrix from SUs' landscape similarities...
# ...Only pixels within tile, each column is a landscape similarity layer
rastile <- terra::as.data.frame(
terra::mask(
ls.rast, tiles[tiles$id == i, ]
),
na.rm = TRUE
)
# Define functions for (row-wise) descending sorting -> Equivalent...
# ...to pixel-wise sorting of SUs' landscape similarity layers
## Find the column index(es) corresponding to n winning SU(s)
fun0 <- function(x) base::order(x, decreasing = TRUE)[1:n.win]
## Find landscape similarity values from the n winning SU(s)
fun1 <- function(x) x[base::order(x, decreasing = TRUE)][1:n.win]
# Apply functions
## Column index for n winning SU(s)
x <- base::apply(rastile, 1, fun0)
## Landscape similarity values for n winning SU(s)
y <- base::apply(rastile, 1, fun1)
# Construct table from column indexes and landscape similarity...
# ...values of n winning SU(s)
z <- data.table::as.data.table(t(base::rbind(x, y)))
## Rename column(s) for n winning SU(s)' column indexes
base::colnames(z)[1:n.win] <- base::paste(
"su_max_",
base::seq(1:n.win),
"_ind",
sep = ""
)
## Rename column(s) for n winning SU(s)' landscape similarity values
base::colnames(z)[(n.win+1):(n.win*2)] <- base::paste(
"su_max_",
base::seq(1:n.win),
"_ls",
sep = ""
)
# Add columns to original landscape similarity matrix
rastile <- base::cbind(rastile, z)
# Remove unnecessary objects
base::remove(fun0, fun1, x, y, z)
# Numeric codes for the n winning SU(s) -> Forcing no prefix in codes
`%do%` <- foreach::`%do%`
sucodes <- foreach::foreach(j = 1:n.win) %do% {
sumax_col <- terra::nlyr(ls.rast) + j
suinds <- base::unlist(base::list(rastile[, sumax_col]))
sucodes <- base::colnames(rastile)[(suinds)]
sucodes <- stringi::stri_replace_all_regex(sucodes, "[a-zA-punct]", "")
sucodes <- base::as.numeric(sucodes)
}
# Set names for column(s) with numeric codes of n winning SU(s)
## String
newnames <- base::paste("su_max_", base::seq(1:n.win), "_code", sep = "")
## Name from string
base::names(sucodes) <- newnames
# Merge column(s) of numeric codes for n winning SU(s)...
# ...with landscape similarity matrix
sucodes <- data.table::as.data.table(sucodes)
rastile <- base::cbind(rastile, sucodes)
# Remove unnecessary objects
base::remove(sumax_col, suinds, sucodes)
# Get the n winning SU(s)' representative observation response values
`%do%` <- foreach::`%do%`
v <- foreach::foreach(k = 1:n.win) %do% {
# Merging landscape similarity matrix with with table of SU(s)'...
# ...representative observation response values -> By column of n...
# ...winning SU' numeric codes
v <- data.table::merge.data.table(
rastile,
su.repobs,
by.x = newnames[k],
by.y = base::colnames(su.repobs)[1],
sort = FALSE,
no.dups = FALSE
)[,
(base::ncol(rastile)+1):(base::ncol(rastile)+n.resp)
]
}
# Format table of n winning SU(s)' representative observation...
# ...response values
v <- data.table::as.data.table(v)
# Assing column names based on n winning SU(s) and response(s)
base::colnames(v) <- base::paste(
"su_max",
base::rep(1:n.win, each = n.resp),
base::colnames(su.repobs)[-1],
sep = "_"
)
# Bind landscape similarity matrix with table of SUs' representative...
# ...observation response values -> Columns corresponding to landscape...
# ...similarity layers and column indexes of winning SU(s) are discarded
rastile <- base::cbind(
rastile[,
(terra::nlyr(ls.rast)+n.win+1):(terra::nlyr(ls.rast)+n.win*2)
],
v
)
base::remove(v)
# Get name(s) for column(s) with landscape similarity values of n...
# ...winning SU(s)
w <- base::colnames(rastile)[1:n.win]
# Get name(s) for column(s) with SU(s)' representative observation...
# ...response values
x <- base::colnames(rastile)[(n.win+1):(base::length(rastile))]
# Get reference raster for writing outputs (crop to processing tile)...
# ...-> Pixel values in this raster will be overwritten by modeled...
# ...response values!
raslay <- terra::mask(ls.rast[[1]], tiles[tiles$id == i, ])
# Get expression(s) for the product of n winning SU's response value...
# ...and landscape similarity value (i.e., weight) -> Equivalent to a...
# ...list of numerators for multiple weighted average expressions
`%do%` <- foreach::`%do%`
y <- foreach::foreach(l = 1:base::length(x)) %do% {
y <- stringdist::amatch(x[l], w, maxDist = 10e3)
y <- base::paste(x[l], w[y], sep = " * ")
}
## Get single list (one weighted average's numerator per response)
y <- base::unlist(y)
# Loop through response(s) to model
`%do%` <- foreach::`%do%`
foreach::foreach(m = base::colnames(su.repobs)[-1]) %do% {
# Get numerator's expression for response -> THIS IS NOT SAFE...
# ...FOR NON-EXPLICIT RESPONSE NAMES
## Response's name matching
z <- y[base::grep(pattern = m, x = y)]
## String collapsing
z <- base::paste(z, collapse = ' + ')
# Form full expression for weighted average -> Nested paste()...
# ...is for denominator
z <- base::paste(
'(', z,')', "/", '(',
base::paste(base::colnames(rastile)[1:n.win], collapse = ' + '),
')',
sep = ""
)
# Model response through weighted average -> Full expression applied...
# ...to landscape similarity matrix
`%>%` <- dplyr::`%>%`
`:=` <- rlang::`:=`
z <- rastile %>% dplyr::transmute(z := !! rlang::parse_expr(z))
# Rewrite reference raster pixel values with modeled response values
raslay[!base::is.na(raslay)] <- base::as.numeric(base::unlist(z))
# Set names for tile of modeled response
## Layer name
layname <- base::paste(m, "_tile", base::as.character(i), sep = "")
## File name
tname <- base::paste(layname, extension, sep = "")
# Save raster tile of modeled response
terra::writeRaster(
raslay,
base::file.path(outdir, tname),
datatype = 'FLT4S',
names = layname,
...
)
# Retrieve file name for tile of modeled response stored in disk
tname <- tname
}
}
# List of tiles for all modeled response(s)
tname <- base::unlist(tname)
# For each modeled response, merge all tiles into single-layer SpatRaster
## [Parallel] processing for multiple responses
response <- foreach::foreach(i = base::colnames(su.repobs)[-1]) %ps% {
# Only tiles for a single response
## List of tiles from same modeled response
tres <- base::grep(pattern = i, tname, value = TRUE)
## Read files from list of tiles from same modeled response
tres <- base::lapply(tres, list.files, path = outdir, full.names = TRUE)
# If tiles = 1
if (base::length(tres) == 1) {
# SpatRaster (tile) with modeled response
tcol <- terra::rast(tres[[1]])
resname <- base::paste(i, extension, sep = "")
tcol <- terra::writeRaster(
tcol,
filename = base::file.path(outdir, resname),
datatype = 'FLT4S',
names = i,
...
)
# Remove tile
base::file.remove(base::file.path(tres))
# Retrieve file name of modeled response stored in disk
response <- resname
} else {
# Create collection of tiles from same modeled response
## Sequential reading of tiles as SpatRaster objects
`%do%` <- foreach::`%do%`
tcol <- foreach::foreach(t = tres) %do% { terra::rast(t) }
## SpatRaster collection
tcol <- terra::sprc(tcol)
# Single-layer SpatRaster of modeled response
## File name
resname <- base::paste(i, extension, sep = "")
## Reduce tiles collection into single-layer SpatRaster
terra::merge(
tcol,
filename = base::file.path(outdir, resname),
datatype = 'FLT4S',
names = i,
...
)
# Remove tiles?
if(tile.rm == TRUE) {
base::file.remove(base::file.path(tres))
} else {
NULL
}
# Retrieve file name of modeled response stored in disk
response <- resname
}
}
# Retrieve raster layer(s) of modeled response(s) from disk
response <- base::unlist(response)
response <- base::paste("^", response, "$", sep = "")
response <- base::lapply(
response, list.files, path = outdir, full.names = TRUE
)
response <- terra::rast(base::unlist(response))
} else if (res.type == "cat") {
# Rewrite name of id field for tiles
base::names(tiles) <- "id"
# Detect number of responses to infer
n.resp <- base::ncol(su.repobs) - 1 # minus id column
# Loop for tiles
tname <- foreach::foreach(i = 1:base::length(tiles)) %ps% {
# Landscape similarity matrix from SUs' landscape similarities...
# ...Only pixels within tile, each column is a landscape similarity layer
rastile <- terra::as.data.frame(
terra::mask(
ls.rast, tiles[tiles$id == i, ]
),
na.rm = TRUE
)
# Define functions for (row-wise) descending sorting -> Equivalent...
# ...to pixel-wise sorting of SUs' landscape similarity layers
## Find the column index(es) corresponding to n winning SU(s)
fun0 <- function(x) base::order(x, decreasing = TRUE)[1:n.win]
## Find landscape similarity values from the n winning SU(s)
fun1 <- function(x) x[base::order(x, decreasing = TRUE)][1:n.win]
# Apply functions
## Column index for n winning SU(s)
x <- base::apply(rastile, 1, fun0)
## landscape similarity values for n winning SU(s)
y <- base::apply(rastile, 1, fun1)
# Construct table from column indexes and landscape similarity...
# ...values of n winning SU(s)
z <- data.table::as.data.table(t(base::rbind(x, y)))
## Rename column(s) for n winning SU(s)' column indexes
base::colnames(z)[1:n.win] <- base::paste(
"su_max_",
base::seq(1:n.win),
"_ind",
sep = ""
)
## Rename column(s) for n winning SU(s)' landscape similarity values
base::colnames(z)[(n.win+1):(n.win*2)] <- base::paste(
"su_max_",
seq(1:n.win),
"_ls",
sep = ""
)
# Add columns to original landscape similarity matrix
rastile <- base::cbind(rastile, z)
# Remove unnecessary objects
base::remove(fun0, fun1, x, y, z)
# Numeric codes for the n winning SU(s) (forcing no prefix in codes)
`%do%` <- foreach::`%do%`
sucodes <- foreach::foreach(j = 1:n.win) %do% {
sumax_col <- terra::nlyr(ls.rast) + j
suinds <- base::unlist(base::list(rastile[, sumax_col]))
sucodes <- base::colnames(rastile)[(suinds)]
sucodes <- stringi::stri_replace_all_regex(sucodes, "[a-zA-punct]", "")
sucodes <- base::as.numeric(sucodes)
}
# Set names for column(s) with numeric codes of n winning SU(s)
## String
newnames <- base::paste("su_max_", base::seq(1:n.win), "_code", sep = "")
## Name from string
base::names(sucodes) <- newnames
# Merge column(s) of numeric codes for n winning SU(s)...
# ...with landscape similarity matrix
sucodes <- data.table::as.data.table(sucodes)
rastile <- base::cbind(rastile, sucodes)
# Remove unnecessary objects
base::remove(sumax_col, suinds, sucodes)
# Get the n winning SU(s)' representative observation response values
`%do%` <- foreach::`%do%`
v <- foreach::foreach(k = 1:n.win) %do% {
# Merging landscape similarity matrix with with table of SU(s)'...
# ...representative observation response values -> By column of n...
# ...winning SU' numeric codes
v <- data.table::merge.data.table(
rastile,
su.repobs,
by.x = newnames[k],
by.y = base::colnames(su.repobs)[1],
sort = FALSE,
no.dups = FALSE
)[,
(base::ncol(rastile)+1):(base::ncol(rastile)+n.resp)
]
}
# Format table of n winning SU(s)' representative observation...
# ...response values
v <- data.table::as.data.table(v)
# Assign column names based on n winning SU(s) and response(s)
base::colnames(v) <- base::paste(
"su_max",
base::rep(1:n.win, each = n.resp),
base::colnames(su.repobs)[-1],
sep = "_"
)
# Remove landscape similarity matrix
base::remove(rastile)
# Get reference raster for writing outputs (crop to processing tile)
# -> Pixel values in this raster will be overwritten by modeled...
# ...response values!
raslay <- terra::mask(ls.rast[[1]], tiles[tiles$id == i, ])
# Loop through response(s) to model
`%do%` <- foreach::`%do%`
foreach::foreach(l = base::colnames(su.repobs)[-1]) %do% {
# Model response through modal value across SU(s)'s representative...
# ...values
cols <- base::grep(pattern = l, x = base::colnames(v))
z <- v[, cols]
## First: Greatest landscape similarity
modfun <- function(x){ terra::modal(x, ties = "first") }
z <- base::apply(z, 1, modfun)
# Rewrite reference raster pixels values with modeled response values
raslay[!base::is.na(raslay)] <- z
# Set names for tile of modeled response
## Layer name
layname <- base::paste(l, "_tile", base::as.character(i), sep = "")
## File name
tname <- base::paste(layname, extension, sep = "")
# Save raster tile of modeled response
terra::writeRaster(
raslay,
base::file.path(outdir, tname),
datatype = 'INT4S',
names = layname,
...
)
# Retrieve file name for tile of modeled response stored in disk
tname <- tname
}
}
# List of tiles for all modeled response(s)
tname <- base::unlist(tname)
# For each modeled response, merge all tiles into single-layer SpatRaster
## [Parallel] processing for multiple responses
response <- foreach::foreach(i = base::colnames(su.repobs)[-1]) %ps% {
# Only tiles for a single response
## List of tiles from same modeled response
tres <- base::grep(pattern = i, tname, value = TRUE)
## Read files from list of tiles from same modeled response
tres <- base::lapply(tres, list.files, path = outdir, full.names = TRUE)
# If tiles = 1
if (base::length(tres) == 1) {
# SpatRaster (tile) with modeled response
tcol <- terra::rast(tres[[1]])
resname <- base::paste(i, extension, sep = "")
tcol <- terra::writeRaster(
tcol,
filename = base::file.path(outdir, resname),
datatype = 'FLT4S',
names = i,
...
)
# Remove tile
base::file.remove(base::file.path(tres))
# Retrieve file name of modeled response stored in disk
response <- resname
} else {
# Create collection of tiles from same modeled response
## Sequential reading of tiles as SpatRaster objects
`%do%` <- foreach::`%do%`
tcol <- foreach::foreach(t = tres) %do% { terra::rast(t) }
## SpatRaster collection
tcol <- terra::sprc(tcol)
# Single-layer SpatRaster of modeled response
## File name
resname <- base::paste(i, extension, sep = "")
## Reduce tiles collection into single-layer SpatRaster
terra::merge(
tcol,
filename = base::file.path(outdir, resname),
datatype = 'INT4S',
names = i,
...
)
# Remove tiles?
if(tile.rm == TRUE) {
base::file.remove(base::file.path(tres))
} else {
NULL
}
# Retrieve file name of modeled response stored in disk
response <- resname
}
}
# Retrieve raster layer(s) of modeled response(s) from disk
response <- base::unlist(response)
response <- base::paste("^", response, "$", sep = "")
response <- base::lapply(
response, list.files, path = outdir, full.names = TRUE
)
response <- terra::rast(base::unlist(response))
} else {
if(verbose == TRUE){
base::warning("Nothing was done. Please select a valid response type.")
}
}
}
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.