From 0f446af5f884a06ed6503091a8dbc83b507be95f Mon Sep 17 00:00:00 2001 From: Daniel Schlaepfer Date: Wed, 1 May 2024 12:21:58 -0400 Subject: [PATCH] Functionality to download, query, and extract soils data from `SOLUS100` --- .Rbuildignore | 1 + .gitignore | 1 + NAMESPACE | 7 + NEWS.md | 7 + R/extract_soils_NRCS_SOLUS100.R | 823 ++++++++++++++++++++++ man/check_SOLUS100.Rd | 52 ++ man/depth_profile_SOLUS100.Rd | 11 + man/download_SOLUS100.Rd | 57 ++ man/extract_soils_SOLUS100.Rd | 181 +++++ man/fetch_soils_from_SOLUS100.Rd | 56 ++ man/filenames_SOLUS100.Rd | 22 + man/variables_SOLUS100.Rd | 11 + tests/testthat/test_soils_NRCS_SOLUS100.R | 98 +++ 13 files changed, 1327 insertions(+) create mode 100644 R/extract_soils_NRCS_SOLUS100.R create mode 100644 man/check_SOLUS100.Rd create mode 100644 man/depth_profile_SOLUS100.Rd create mode 100644 man/download_SOLUS100.Rd create mode 100644 man/extract_soils_SOLUS100.Rd create mode 100644 man/fetch_soils_from_SOLUS100.Rd create mode 100644 man/filenames_SOLUS100.Rd create mode 100644 man/variables_SOLUS100.Rd create mode 100644 tests/testthat/test_soils_NRCS_SOLUS100.R diff --git a/.Rbuildignore b/.Rbuildignore index 7014972..5c5a363 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -15,3 +15,4 @@ ^cran-comments\.md$ ^NEWS\.md$ ^tests/test_data/NED1 +^tests/test_data/SOLUS100 diff --git a/.gitignore b/.gitignore index 0cc7c69..3c9f237 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,5 @@ vignettes/*.pdf vignettes/*.html # Local test data objects +tests/test_data/SOLUS100 tests/test_data/NED1 diff --git a/NAMESPACE b/NAMESPACE index 48231f3..73db7c8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,18 +3,25 @@ export(calculate_soil_depth_NRCS) export(check_Miller1998_CONUSSoil) export(check_POLARIS) +export(check_SOLUS100) export(create_conditioned_Miller1998_CONUSSoil) +export(depth_profile_SOLUS100) +export(download_SOLUS100) export(extract_soils_Miller1998_CONUSSoil) export(extract_soils_NRCS_SDA) export(extract_soils_POLARIS) +export(extract_soils_SOLUS100) export(extract_topography_NEDUSA) export(fetch_mukeys_spatially_NRCS_SDA) export(fetch_soils_from_Miller1998_CONUSSoil) export(fetch_soils_from_NRCS_SDA) export(fetch_soils_from_POLARIS) +export(fetch_soils_from_SOLUS100) +export(filenames_SOLUS100) export(is_NRCS_horizon_organic) export(prepare_script_for_Miller1998_CONUSSoil) export(prepare_script_for_POLARIS) +export(variables_SOLUS100) import(methods) importFrom(stats,aggregate) importFrom(stats,coef) diff --git a/NEWS.md b/NEWS.md index e891f01..59043da 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,11 @@ # rSW2exter v0.3.0-9000 +* Functionality to download, query, and extract soils data from `SOLUS100` + * `depth_profile_SOLUS100()`, `variables_SOLUS100()`, and + `filenames_SOLUS100()` provide meta data. + * `download_SOLUS100()` and `check_SOLUS100()` download and manage + a local copy. + * `extract_soil_SOLUS100()` (and bare-bones `fetch_soils_from_SOLUS100()`) + extract soils data from a local copy. # rSW2exter v0.2.2 diff --git a/R/extract_soils_NRCS_SOLUS100.R b/R/extract_soils_NRCS_SOLUS100.R new file mode 100644 index 0000000..e96e9c8 --- /dev/null +++ b/R/extract_soils_NRCS_SOLUS100.R @@ -0,0 +1,823 @@ +create_reference_for_SOLUS100 <- function() { + paste0( + "Nauman, T. 2024. ", + "Data from: Soil Landscapes of the United States 100-meter (`SOLUS100`) ", + "soil property maps project repository. Ag Data Commons.", + "https://doi.org/10.15482/USDA.ADC/25033856.V1.", + "Accessed [", format(Sys.Date(), "%Y-%b-%e"), "]" + ) +} + +#' List of variables available from `SOLUS100` +#' @md +#' @export +variables_SOLUS100 <- function() { + data.frame( + type = c("depth", "depth", rep("property", 18L)), + name = c( + "anylithicdpt_cm", "resdept_cm", + "caco3", + "cec7", + "claytotal", + "dbovendry", + "ec", "ecec", + "fragvol", + "gypsum", + "ph1to1h2o", + "sandco", "sandfine", "sandmed", "sandtotal", "sandvc", "sandvf", + "sar", + "silttotal", + "soc" + ), + scaling_factor = c( + 1, 1, # depth + 100, # caco3 [% -> fraction] + 10, # cec7, + 100, # claytotal [% -> fraction] + 100, # dbovendry + 10, 10, # ec, ecec + 100, # fragvol [% -> fraction] + 100, # gypsum [% -> fraction] + 100, # ph1to1h2o + rep(100, 6L), # sand* [% -> fraction] + 1, # sar + 100, # silttotal [% -> fraction] + 100 # soc [% -> fraction] + ), + stringsAsFactors = FALSE + ) +} + + +#' List of soil depths available from `SOLUS100` +#' @md +#' @export +depth_profile_SOLUS100 <- function() { + c(0L, 5L, 15L, 30L, 60L, 100L, 150L) +} + + +#' Compose a file name of `SOLUS100` +#' +#' @param vars A vector of variable names. See [variables_SOLUS100()] +#' @param depths Soil depths in centimeters from surface. +#' See [depth_profile_SOLUS100()] +#' @param stat A vector of character strings. See Nauman et al. 2024 +#' +#' @return A vector of file names. +#' +#' @md +#' @export +filenames_SOLUS100 <- function(vars, depths, stat) { + tmp <- variables_SOLUS100() + tmp <- tmp[tmp[["type"]] == "depth", "name", drop = TRUE] + vars_depth <- intersect(tmp, vars) + vars_bylayer <- setdiff(vars, tmp) + + c( + if (length(vars_depth) > 0L) paste0(vars_depth, "_", stat, ".tif"), + if (length(vars_bylayer) > 0L) { + vapply( + depths, + function(d) paste0(vars_bylayer, "_", d, "_cm_", stat, ".tif"), + FUN.VALUE = rep(NA_character_, times = length(vars_bylayer)) + ) + } + ) +} + + +#' Check local copy of `SOLUS100` +#' +#' @param path A character string. The path to the local copy of `SOLUS100`. +#' @inheritParams filenames_SOLUS100 +#' +#' @references +#' Nauman, T. 2024. +#' Data from: Soil Landscapes of the United States 100-meter (`SOLUS100`) +#' soil property maps project repository. Ag Data Commons. +#' . +#' +#' @seealso [download_SOLUS100()] +#' +#' @examples +#' dir_tmp <- tempdir() +#' has_solus <- check_SOLUS100(dir_tmp, vars = "resdept_cm") +#' +#' \dontrun{ +#' if (curl::has_internet()) { +#' fns_solus <- download_SOLUS100(dir_tmp, vars = "resdept_cm") +#' files_solus <- file.path(dir_tmp, fns_solus) +#' terra::plot(terra::rast(files_solus)) +#' has_solus <- check_SOLUS100(dir_tmp, vars = "resdept_cm") +#' +#' unlink(files_solus) # clean up +#' } +#' } +#' +#' @md +#' @export +check_SOLUS100 <- function( + path = ".", + vars = c( + "resdept_cm", + "dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", "soc" + ), + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi") +) { + vars <- intersect(vars, variables_SOLUS100()[["name"]]) + depths <- intersect(depths, depth_profile_SOLUS100()) + stat <- match.arg(stat) + + #--- Put together requested layer names + requested_filenames <- filenames_SOLUS100(vars, depths, stat) + + #--- Check which requested layers are (not) already downloaded + res <- file.exists(file.path(path, requested_filenames)) + names(res) <- requested_filenames + res +} + + +#' Download soil layers from `SOLUS100` +#' +#' @inheritParams filenames_SOLUS100 +#' @inheritParams check_SOLUS100 +#' @param url_solus100 The `URL` to the `SOLUS100` data repository. +#' See Nauman et al. 2024 +#' @param verbose A logical value. +#' +#' @return File names to local copies of requested soil layers. +#' +#' @references +#' Nauman, T. 2024. +#' Data from: Soil Landscapes of the United States 100-meter (`SOLUS100`) +#' soil property maps project repository. Ag Data Commons. +#' . +#' +#' @seealso [check_SOLUS100()] +#' +#' @examples +#' \dontrun{ +#' if (curl::has_internet()) { +#' dir_tmp <- tempdir() +#' fsolus <- download_SOLUS100(dir_tmp, vars = "resdept_cm") +#' terra::plot(terra::rast(file.path(dir_tmp, fsolus))) +#' unlink(file.path(dir_tmp, fsolus)) # clean up +#' } +#' } +#' +#' @md +#' @export +download_SOLUS100 <- function( + path = ".", + vars = c( + "resdept_cm", + "dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", "soc" + ), + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi"), + url_solus100 = "https://storage.googleapis.com/solus100pub/", + verbose = FALSE +) { + dir.create(path, recursive = TRUE, showWarnings = FALSE) + + vars <- intersect(vars, variables_SOLUS100()[["name"]]) + depths <- intersect(depths, depth_profile_SOLUS100()) + stat <- match.arg(stat) + + #--- Check which requested layers are (not) already downloaded + has_filenames <- check_SOLUS100(path, vars, depths, stat) + requested_filenames <- names(has_filenames) + + todo_filenames <- requested_filenames[!has_filenames] + + + #--- Download needed files + Nall <- length(requested_filenames) + Ntodo <- length(todo_filenames) + + if (verbose) { + pb <- utils::txtProgressBar(max = Nall, style = 3L) + vpbi <- Nall - Ntodo + utils::setTxtProgressBar(pb, value = vpbi) + } + + for (k in seq_len(Ntodo)) { + try( + utils::download.file( + url = paste0(url_solus100, todo_filenames[[k]]), + destfile = file.path(path, todo_filenames[[k]]), + quiet = TRUE + ) + ) + + if (verbose) utils::setTxtProgressBar(pb, value = vpbi + k) + } + + if (verbose) close(pb) + + intersect( + list.files(path, pattern = ".tif$", recursive = FALSE), + requested_filenames + ) +} + + + +#' Extract soil information from the `SOLUS100` soil dataset +#' +#' @inheritParams check_SOLUS100 +#' @inheritParams rSW2st::as_points +#' @param fun A function. Summarizing gridcell values if more than one value +#' is extracted per location. See [terra::extract()]. +#' @param na.rm A logical value. Passed to `fun`. +#' @param verbose A logical value. +#' +#' @section Notes: This is a function with minimal functionality; +#' use [extract_soils_SOLUS100()] for a user-friendly interface. +#' +#' @references +#' Nauman, T. 2024. +#' Data from: Soil Landscapes of the United States 100-meter (`SOLUS100`) +#' soil property maps project repository. Ag Data Commons. +#' . +#' +#' @md +#' @export +fetch_soils_from_SOLUS100 <- function( + x, + vars, + depths, + stat, + path = ".", + fun = NULL, + na.rm = TRUE, + verbose = FALSE +) { + #--- Prepare result object + res <- array( + NA, + dim = c(nrow(x), length(vars), length(depths)), + dimnames = list(NULL, vars, depths) + ) + + #--- Extract values + for (iv in seq_along(vars)) { + ftmp <- file.path(path, filenames_SOLUS100(vars[iv], depths, stat)) + + if (all(file.exists(ftmp))) { + list_args <- list( + x = terra::rast(ftmp), + y = x, + ID = FALSE, + raw = TRUE + ) + + if (!is.null(fun)) { + list_args <- c(list_args, list(fun = fun, na.rm = na.rm)) + } + + tmp <- do.call(terra::extract, args = list_args) + + res[, iv, seq_along(ftmp)] <- data.matrix(tmp) + + } else { + stop("SOLUS100 data ", toString(shQuote(basename(ftmp))), " not found.") + } + } + + res +} + + + +#' Extract soil information from the `SOLUS100` soil dataset +#' for \pkg{SOILWAT2} applications +#' +#' @inheritParams check_SOLUS100 +#' @inheritParams fetch_soils_from_SOLUS100 +#' @inheritParams rSW2st::as_points +#' @param var_depth A character string or `NULL`. +#' If `NULL`, then soil depth is determined based on +#' (i) available data layers `depths` (up to 201 cm; see Nauman et al. 2024), +#' (ii) depth of non-missing values in extracted `vars` (which may be 0). +#' Otherwise, soil depth is the extracted value of the `SOLUS100` +#' variable `var_depth`. +#' Soil depth is set to 0 if is missing or all `vars` values are missing. +#' @param method_vertical A character string that is +#' `"asis"` or `"interpolate_by_layer"`: +#' (i) method `"asis"` extracts the values as provided by `SOLUS100`, +#' i.e., as point estimates at specified depths +#' (ii) method `"interpolate_by_layer"` interpolates extracted values +#' for each centimeter depth increment and averages +#' the interpolated values across requested soil layers +#' (see `requested_layer_depths`). +#' Soils properties for a soil with a missing depth value or a +#' depth of less than 1 centimeter are set to `NA`. +#' @param requested_layer_depths An integer vector +#' (used if `method_vertical = "interpolate_by_layer"`). +#' Soil depths (in centimeters) at lower layer boundaries used for output +#' If `NULL`, then layer boundaries are assumed to be `c(depths, 201)`. +#' @param method_horizontal A character string. +#' Method that determines the extraction approach across grid cells: +#' (i) values are extracted using arguments +#' `buffer_m`, `fun`, and `na.rm` +#' and are returned `"asis"` or +#' (ii) values are extracted for point locations, +#' i.e., temporarily setting `buffer_m = NULL`; then, +#' sites with problematic values (as determined by `fix_criteria`) +#' are extracted again under `"fix_with_buffer"` based on +#' `buffer_m`, `fun`, and `na.rm` +#' @param fix_criteria A named list +#' (used if `method_horizontal = "fix_with_buffer"`). +#' Names match values of `vars` or one of the names can be `"texture"` +#' (in which case the criterion is applied to sand, clay, and silt). +#' Each element is applied to the variable that corresponds to the name +#' and is used to determine whether a site has problematic values. +#' Elements themselves are a named list with two sub-elements +#' `"op"` for the relationship operator, e.g., `"<"`, and +#' `"value"` for the value to compare against. See examples. +#' @param buffer_m A numeric value. The radius of a buffer around each point +#' from which to extract cell values and across which `fun` is applied. +#' Set to `NULL` to extract `SOLUS100` gridcell values at point locations. +#' @param fun A function or a named list containing functions +#' (used if `method_horizontal = "fix_with_buffer"`). +#' Names match values of `vars` or one of the names can be `"texture"` +#' (in which case the criterion is applied to sand, clay, and silt). +#' The function(s) summarize(s) extracted values if more than one value +#' is extracted per location (based on `buffer_m`). +#' @param digits An integer value. The number of digits to which soil texture +#' variables are rounded. Skip rounding if `NA` or `NULL`. +#' +#' @section Notes: A local copy of `SOLUS100` is required. The function +#' [download_SOLUS100()] can be used to download `SOLUS100` files. +#' +#' @references +#' Nauman, T. 2024. +#' Data from: Soil Landscapes of the United States 100-meter (`SOLUS100`) +#' soil property maps project repository. Ag Data Commons. +#' . +#' +#' @seealso [terra::extract()] +#' +#' @examples +#' \dontrun{ +#' if (curl::has_internet()) { +#' path_solus100 <- tempdir() +#' req_vars <- c("resdept_cm", "sandtotal") +#' req_depths <- 0 +#' +#' ## Download data +#' fns_solus100 <- rSW2exter::download_SOLUS100( +#' path = path_solus100, +#' vars = req_vars, +#' depths = req_depths +#' ) +#' +#' ## Check that we have SOLUS100 data +#' has_SOLUS100 <- isTRUE(all( +#' check_SOLUS100( +#' path = path_solus100, +#' vars = req_vars, +#' depths = req_depths +#' ) +#' )) +#' +#' if (has_SOLUS100) { +#' locations <- matrix( +#' data = c(-120.1286878, -111.8511136, 39.8182913, 36.9047396), +#' nrow = 2 +#' ) +#' +#' ## Extract gridcell values at point locations +#' res <- extract_soils_SOLUS100( +#' x = locations, +#' vars = req_vars, +#' depths = req_depths, +#' path = path_solus100 +#' ) +#' } +#' +#' # Clean up example +#' unlink(file.path(path_solus100, fns_solus100)) +#' } +#' } +#' +#' @md +#' @export +extract_soils_SOLUS100 <- function( + x, + crs = 4326, + vars = c( + "dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", "soc" + ), + var_depth = "resdept_cm", + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi"), + path = ".", + method_vertical = c("asis", "interpolate_by_layer"), + requested_layer_depths = NULL, + method_horizontal = c("asis", "fix_with_buffer"), + fix_criteria = list( + dbovendry = list(op = "<", value = 0.6), + texture = list(op = "<", value = 0.5) + ), + buffer_m = NULL, fun = NULL, na.rm = TRUE, + digits = 3L, + verbose = FALSE +) { + + vars_solus100 <- variables_SOLUS100() + max_depth <- 201L # see Nauman et al. 2024 + + #--- * Make sure inputs are correctly formatted ------ + stat <- match.arg(stat) + method_vertical <- match.arg(method_vertical) + method_horizontal <- match.arg(method_horizontal) + + var_depth <- var_depth[[1L]] + tmp <- intersect(c(var_depth, vars), vars_solus100[["name"]]) + if (!setequal(tmp, vars)) { + warning( + "Ignoring requested variables ", + toString(shQuote(setdiff(vars, vars_solus100[["name"]]))), + " that are not provided by SOLUS100." + ) + } + vars <- tmp + + has_rld <- length(requested_layer_depths) > 0L + requested_layer_depths <- sort(as.integer(requested_layer_depths)) + if (any(requested_layer_depths <= 0L)) { + warning( + "Ignoring requested soil layers shallower than minimum depth of 1 cm." + ) + ids <- requested_layer_depths > 0L + requested_layer_depths <- requested_layer_depths[ids] + } + if (any(requested_layer_depths > max_depth)) { + warning( + "Ignoring requested soil layers deeper than maximum depth of ", + max_depth, " cm; assigning maximum depth as boundary of deepest layer." + ) + ids <- requested_layer_depths <= max_depth + requested_layer_depths <- c(requested_layer_depths[ids], max_depth) + } + if (has_rld && length(requested_layer_depths) == 0L) { + stop("Failed to process 'requested_layer_depths'.") + } + + depths <- sort(as.integer(depths)) + tmp <- intersect(depths, depth_profile_SOLUS100()) + if (!setequal(tmp, depths)) { + warning( + "Ignoring requested depths ", + toString(shQuote(setdiff(depths, depth_profile_SOLUS100()))), + " that are not provided by SOLUS100." + ) + } + depths <- tmp + + #--- * Identify variables ------ + has_solus100 <- check_SOLUS100(path, vars, depths, stat) + stopifnot(has_solus100) + + tmp <- vars_solus100[vars_solus100[["type"]] == "depth", "name", drop = TRUE] + vars_depth <- intersect(tmp, vars) + vars_bylayer <- setdiff(vars, tmp) + + var_stxt3 <- c("sandtotal", "silttotal", "claytotal") + var_stxt <- intersect(var_stxt3, vars) + var_others <- setdiff(vars, c(var_stxt, vars_depth)) + + #--- * Transform locations to CRS of SOLUS100 ------ + locations <- sf::st_transform( + rSW2st::as_points(x, to_class = "sf", crs = crs), + sf::st_crs( + terra::rast( + file.path(path, filenames_SOLUS100(vars[[1L]], depths[[1L]], stat)) + ) + ) + ) + + N_sites <- nrow(locations) + + #--- * Extract values from SOLUS100 ------ + res <- fetch_soils_from_SOLUS100( + x = locations, + vars = vars, + depths = depths, + stat = stat, + path = path, + fun = if (identical(method_horizontal, "fix_with_buffer")) NULL else fun, + na.rm = na.rm, + verbose = verbose + ) + + + #--- * Replace sites with problematic values by buffered extractions ------ + if (identical(method_horizontal, "fix_with_buffer")) { + locs_buffered <- sf::st_buffer(locations, dist = buffer_m) + + # Determine for which variables we have criteria to determine problems + tmp <- intersect(c(vars, "texture"), names(fix_criteria)) + ok <- vapply( + X = fix_criteria[tmp], + FUN = function(x) all(c("op", "value") %in% names(x)), + FUN.VALUE = NA + ) + check_vars <- tmp[ok] + + # Is `fix_criteria` well formed? + if (!all(ok)) { + warning( + "Cannot apply `fix_with_buffer` for ", + toString(shQuote(tmp[!ok])), + " because of incomplete criteria." + ) + } + + + # Determine whether we have one `fun` to be applied to all fixes or + # separate `fun`s + one_fun <- !is.list(fun) && is.function(try(match.fun(fun), silent = TRUE)) + ok <- if (one_fun) TRUE else check_vars %in% names(fun) + + # Is `fun` well formed? + if (!all(ok)) { + warning( + "Cannot apply `fix_with_buffer` for ", + toString(shQuote(tmp[!ok])), + " because of missing summarizing function `fun`." + ) + } + + + # Fix for texture variables + if ("texture" %in% check_vars) { + hasnot_texture <- !(var_stxt3 %in% vars) + + if (any(hasnot_texture)) { + warning( + "Cannot apply `fix_with_buffer` for `texture` because of ", + "missing texture variables: ", + toString(shQuote(var_stxt3[hasnot_texture])) + ) + + } else { + tmp <- fix_criteria[["texture"]] + + is_missing <- apply(res[, var_stxt3, , drop = FALSE], 1L, anyNA) + + is_bad_texture <- apply( + X = apply(res[, var_stxt3, , drop = FALSE], c(1L, 3L), sum), + MARGIN = 1L, + FUN = function(x) { + any(do.call(tmp[["op"]], args = list(x, tmp[["value"]]))) + } + ) + + ids_fix_with_buffer <- which(is_missing | is_bad_texture) + + if (length(ids_fix_with_buffer) > 0L) { + res[ids_fix_with_buffer, var_stxt3, ] <- fetch_soils_from_SOLUS100( + x = locs_buffered[ids_fix_with_buffer, , drop = FALSE], + vars = var_stxt3, + depths = depths, + stat = stat, + path = path, + fun = if (one_fun) fun else fun[["texture"]], + na.rm = na.rm, + verbose = verbose + ) + } + } + + check_vars <- grep( + "texture", + x = check_vars, + value = TRUE, + invert = TRUE, + fixed = TRUE + ) + } + + # Fix for all other variables + for (k in seq_along(check_vars)) { + tmp <- fix_criteria[[check_vars[k]]] + + is_missing <- apply(res[, check_vars[k], , drop = FALSE], 1L, anyNA) + + is_bad <- apply( + X = res[, check_vars[k], , drop = FALSE], + MARGIN = 1L, + FUN = function(x) { + any(do.call(tmp[["op"]], args = list(x, tmp[["value"]]))) + } + ) + + ids_fix_with_buffer <- which(is_missing | is_bad) + + if (length(ids_fix_with_buffer) > 0L) { + res[ids_fix_with_buffer, check_vars[k], ] <- fetch_soils_from_SOLUS100( + x = locs_buffered[ids_fix_with_buffer, , drop = FALSE], + vars = check_vars[k], + depths = depths, + stat = stat, + path = path, + fun = if (one_fun) fun else fun[[check_vars[k]]], + na.rm = na.rm, + verbose = verbose + ) + } + } + } + + + #--- * Apply scaling value ------ + vars_res <- dimnames(res)[[2L]] + + ids <- match(vars_res, vars_solus100[["name"]], nomatch = 0L) + for (k in seq_along(vars_res)) { + res[, k, ] <- res[, k, ] / vars_solus100[ids[[k]], "scaling_factor"] + } + + + #--- * Identify soil depth to nearest centimeter ------ + ids <- apply( + res[, vars_bylayer, , drop = FALSE], + MARGIN = 1L, + FUN = function(x) min(rowSums(!is.na(x))) + ) + + if (length(vars_depth) > 0L) { + tmp <- if (var_depth %in% vars) var_depth else vars_depth[[1L]] + soildepth <- round(res[, tmp, 1L, drop = TRUE]) + # Set soil depth to 0 if missing or all properties are missing + soildepth[is.na(soildepth) | ids == 0L] <- 0L + + res_depths <- array( + round(res[, vars_depth, 1L]), + dim = c(N_sites, length(vars_depth)), + dimnames = list(NULL, vars_depth) + ) + + } else { + soildepth <- c(depths[-1L], max_depth)[ids] + res_depths <- NULL + } + + + #--- * Apply vertical method ------ + if (identical(method_vertical, "interpolate_by_layer")) { + if (is.null(requested_layer_depths)) { + requested_layer_depths <- setdiff(c(depths, max_depth), 0L) + } + + dr <- dim(res) + N_layers1 <- dr[[3L]] + N_layers2 <- length(requested_layer_depths) + + # Add soil depth to array of extracted values + tmp1 <- array( + dim = c(dr[1:2], N_layers1 + 1L), + dimnames = c(dimnames(res)[1:2], list(c(depths, "soildepth"))) + ) + tmp1[, , seq_along(depths)] <- res + tmp1[, , length(depths) + 1L] <- soildepth + + # Interpolate in 1-cm depth intervals and average to requested soil layers + # return NAs if soil depth is missing or less than 1 cm + tmp2 <- apply( + tmp1[, vars_bylayer, , drop = FALSE], + MARGIN = c(1L, 2L), + FUN = function(x) { + # x[1:N_layers1] represents soil property + # x[N_layers1 + 1L] represents soil depth + res <- if (isTRUE(x[[N_layers1 + 1L]] >= 1L)) { + md <- min(x[[N_layers1 + 1L]], max(requested_layer_depths)) + d1s <- seq.int(from = 1L, to = md, by = 1L) + itvs <- findInterval( + d1s, + vec = c(0L, requested_layer_depths), + left.open = TRUE + ) + + tmpa <- stats::approx( + x = depths, + y = x[seq_len(N_layers1)], + xout = d1s, + rule = c(1L, 2L) # NA if < 0, last value if > 150 + ) + + tapply(tmpa[["y"]], itvs, mean) + } + + c(res, rep(NA, times = N_layers2 - length(res))) + } + ) + + res <- aperm(tmp2, perm = c(2L, 3L, 1L)) + rm(tmp1, tmp2) + used_depths <- requested_layer_depths + + } else { + res <- res[, vars_bylayer, , drop = FALSE] # remove depth from properties + used_depths <- depths + } + + + #--- * Rounding ------ + N_layers <- dim(res)[[3L]] + + if (is.finite(digits)) { + if (all(var_stxt3 %in% vars_res)) { + for (k in seq_len(N_layers)) { + has_vals <- + complete.cases(res[, var_stxt3, k]) & + rowSums(res[, var_stxt3, k, drop = FALSE], na.rm = TRUE) > 0 + + res[has_vals, var_stxt3, k] <- rSW2utils::scale_rounded_by_sum( + x = res[has_vals, var_stxt3, k], + digits = digits, + icolumn_adjust = 3 + ) + } + + var_others2 <- var_others + + } else { + var_others2 <- c(var_others, var_stxt) + } + + var_others2 <- intersect(var_others2, vars_res) + res[, var_others2, ] <- round(res[, var_others2, ], digits) + } + + + #--- * Create soil depth output table ------ + tmpd <- matrix( + data = used_depths, + nrow = N_sites, + ncol = N_layers, + byrow = TRUE, + dimnames = list(NULL, paste0("depth_L", seq_len(N_layers))) + ) + + if (identical(method_vertical, "interpolate_by_layer")) { + #--- Set lowest layer depth to soil depth + tmp2 <- cbind(soildepth, tmpd) + + # Identify layer index that contains soil depth + L_at_soildepth <- apply( + X = tmp2, + MARGIN = 1L, + FUN = function(x) { + findInterval(x[[1L]], c(0L, na.exclude(x[-1L])), left.open = TRUE) + } + ) + ids <- which( + !apply( + X = tmp2, + MARGIN = 1L, + FUN = function(x) { + x[[1L]] == 0L || all(is.na(x[-1])) || x[[1L]] %in% x[-1L] + } + ) + ) + + # Update lower boundary of layer to be soil depth + tmpd[cbind(ids, L_at_soildepth[ids])] <- soildepth[ids] + tmpd[soildepth < tmpd] <- NA_integer_ # Set layers below soil depth to NA + } + + + #--- Put final table together + locs_table_depths <- cbind( + N_horizons = rowSums(!is.na(tmpd)), # Count number of soil layers + SoilDepth_cm = soildepth, + if (!is.null(res_depths)) res_depths, + tmpd + ) + + + #--- * Create texture output table ------ + + # Convert to wide format (one row for each point location) + locs_table_texture <- reshape2::acast(reshape2::melt(res), Var1 ~ Var3 + Var2) + colnames(locs_table_texture) <- paste0( + rep(vars_bylayer, times = N_layers), + "_L", + rep(seq_len(N_layers), each = length(vars_bylayer)) + ) + + + #--- Return tables + list( + ref = create_reference_for_SOLUS100(), + table_depths = locs_table_depths, + table_texture = locs_table_texture + ) +} diff --git a/man/check_SOLUS100.Rd b/man/check_SOLUS100.Rd new file mode 100644 index 0000000..d3c1ffe --- /dev/null +++ b/man/check_SOLUS100.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{check_SOLUS100} +\alias{check_SOLUS100} +\title{Check local copy of \code{SOLUS100}} +\usage{ +check_SOLUS100( + path = ".", + vars = c("resdept_cm", "dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", + "soc"), + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi") +) +} +\arguments{ +\item{path}{A character string. The path to the local copy of \code{SOLUS100}.} + +\item{vars}{A vector of variable names. See \code{\link[=variables_SOLUS100]{variables_SOLUS100()}}} + +\item{depths}{Soil depths in centimeters from surface. +See \code{\link[=depth_profile_SOLUS100]{depth_profile_SOLUS100()}}} + +\item{stat}{A vector of character strings. See Nauman et al. 2024} +} +\description{ +Check local copy of \code{SOLUS100} +} +\examples{ +dir_tmp <- tempdir() +has_solus <- check_SOLUS100(dir_tmp, vars = "resdept_cm") + +\dontrun{ +if (curl::has_internet()) { + fns_solus <- download_SOLUS100(dir_tmp, vars = "resdept_cm") + files_solus <- file.path(dir_tmp, fns_solus) + terra::plot(terra::rast(files_solus)) + has_solus <- check_SOLUS100(dir_tmp, vars = "resdept_cm") + + unlink(files_solus) # clean up +} +} + +} +\references{ +Nauman, T. 2024. +Data from: Soil Landscapes of the United States 100-meter (\code{SOLUS100}) +soil property maps project repository. Ag Data Commons. +\url{https://doi.org/10.15482/USDA.ADC/25033856.V1}. +} +\seealso{ +\code{\link[=download_SOLUS100]{download_SOLUS100()}} +} diff --git a/man/depth_profile_SOLUS100.Rd b/man/depth_profile_SOLUS100.Rd new file mode 100644 index 0000000..4ae08be --- /dev/null +++ b/man/depth_profile_SOLUS100.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{depth_profile_SOLUS100} +\alias{depth_profile_SOLUS100} +\title{List of soil depths available from \code{SOLUS100}} +\usage{ +depth_profile_SOLUS100() +} +\description{ +List of soil depths available from \code{SOLUS100} +} diff --git a/man/download_SOLUS100.Rd b/man/download_SOLUS100.Rd new file mode 100644 index 0000000..959a057 --- /dev/null +++ b/man/download_SOLUS100.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{download_SOLUS100} +\alias{download_SOLUS100} +\title{Download soil layers from \code{SOLUS100}} +\usage{ +download_SOLUS100( + path = ".", + vars = c("resdept_cm", "dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", + "soc"), + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi"), + url_solus100 = "https://storage.googleapis.com/solus100pub/", + verbose = FALSE +) +} +\arguments{ +\item{path}{A character string. The path to the local copy of \code{SOLUS100}.} + +\item{vars}{A vector of variable names. See \code{\link[=variables_SOLUS100]{variables_SOLUS100()}}} + +\item{depths}{Soil depths in centimeters from surface. +See \code{\link[=depth_profile_SOLUS100]{depth_profile_SOLUS100()}}} + +\item{stat}{A vector of character strings. See Nauman et al. 2024} + +\item{url_solus100}{The \code{URL} to the \code{SOLUS100} data repository. +See Nauman et al. 2024} + +\item{verbose}{A logical value.} +} +\value{ +File names to local copies of requested soil layers. +} +\description{ +Download soil layers from \code{SOLUS100} +} +\examples{ +\dontrun{ +if (curl::has_internet()) { + dir_tmp <- tempdir() + fsolus <- download_SOLUS100(dir_tmp, vars = "resdept_cm") + terra::plot(terra::rast(file.path(dir_tmp, fsolus))) + unlink(file.path(dir_tmp, fsolus)) # clean up +} +} + +} +\references{ +Nauman, T. 2024. +Data from: Soil Landscapes of the United States 100-meter (\code{SOLUS100}) +soil property maps project repository. Ag Data Commons. +\url{https://doi.org/10.15482/USDA.ADC/25033856.V1}. +} +\seealso{ +\code{\link[=check_SOLUS100]{check_SOLUS100()}} +} diff --git a/man/extract_soils_SOLUS100.Rd b/man/extract_soils_SOLUS100.Rd new file mode 100644 index 0000000..296e408 --- /dev/null +++ b/man/extract_soils_SOLUS100.Rd @@ -0,0 +1,181 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{extract_soils_SOLUS100} +\alias{extract_soils_SOLUS100} +\title{Extract soil information from the \code{SOLUS100} soil dataset +for \pkg{SOILWAT2} applications} +\usage{ +extract_soils_SOLUS100( + x, + crs = 4326, + vars = c("dbovendry", "fragvol", "sandtotal", "silttotal", "claytotal", "soc"), + var_depth = "resdept_cm", + depths = depth_profile_SOLUS100(), + stat = c("p", "l", "h", "rpi"), + path = ".", + method_vertical = c("asis", "interpolate_by_layer"), + requested_layer_depths = NULL, + method_horizontal = c("asis", "fix_with_buffer"), + fix_criteria = list(dbovendry = list(op = "<", value = 0.6), texture = list(op = "<", + value = 0.5)), + buffer_m = NULL, + fun = NULL, + na.rm = TRUE, + digits = 3L, + verbose = FALSE +) +} +\arguments{ +\item{x}{A numerical two-dimensional object +(a \code{matrix}, \code{array}, or \code{data.frame}) +with longitude/X, latitude/Y as columns; +a \code{\link[sp:SpatialPoints-class]{sp::SpatialPoints}} object; or +a \code{\link[terra:SpatVector-class]{terra::SpatVector}} object; or +a \var{sf} object with a point geometry, +i.e., an object with a class \var{sf} or \var{sfc}.} + +\item{crs}{An object which is a \var{crs} or from which one can be derived. +\code{x} can +be numeric as a \var{EPSG} number; +a character string as a \var{wkt}; +a character string as a \var{proj4} (not recommended because outdated); +or of a class including + \code{\link[raster:Raster-class]{raster::Raster}}, + \code{\link[sp:Spatial-class]{sp::Spatial}}, + \code{\link[sp:CRS-class]{sp::CRS}}, + or a \code{\link[sf]{sf}} or \code{\link[sf]{sfc}} class.} + +\item{vars}{A vector of variable names. See \code{\link[=variables_SOLUS100]{variables_SOLUS100()}}} + +\item{var_depth}{A character string or \code{NULL}. +If \code{NULL}, then soil depth is determined based on +(i) available data layers \code{depths} (up to 201 cm; see Nauman et al. 2024), +(ii) depth of non-missing values in extracted \code{vars} (which may be 0). +Otherwise, soil depth is the extracted value of the \code{SOLUS100} +variable \code{var_depth}. +Soil depth is set to 0 if is missing or all \code{vars} values are missing.} + +\item{depths}{Soil depths in centimeters from surface. +See \code{\link[=depth_profile_SOLUS100]{depth_profile_SOLUS100()}}} + +\item{stat}{A vector of character strings. See Nauman et al. 2024} + +\item{path}{A character string. The path to the local copy of \code{SOLUS100}.} + +\item{method_vertical}{A character string that is +\code{"asis"} or \code{"interpolate_by_layer"}: +(i) method \code{"asis"} extracts the values as provided by \code{SOLUS100}, +i.e., as point estimates at specified depths +(ii) method \code{"interpolate_by_layer"} interpolates extracted values +for each centimeter depth increment and averages +the interpolated values across requested soil layers +(see \code{requested_layer_depths}). +Soils properties for a soil with a missing depth value or a +depth of less than 1 centimeter are set to \code{NA}.} + +\item{requested_layer_depths}{An integer vector +(used if \code{method_vertical = "interpolate_by_layer"}). +Soil depths (in centimeters) at lower layer boundaries used for output +If \code{NULL}, then layer boundaries are assumed to be \code{c(depths, 201)}.} + +\item{method_horizontal}{A character string. +Method that determines the extraction approach across grid cells: +(i) values are extracted using arguments +\code{buffer_m}, \code{fun}, and \code{na.rm} +and are returned \code{"asis"} or +(ii) values are extracted for point locations, +i.e., temporarily setting \code{buffer_m = NULL}; then, +sites with problematic values (as determined by \code{fix_criteria}) +are extracted again under \code{"fix_with_buffer"} based on +\code{buffer_m}, \code{fun}, and \code{na.rm}} + +\item{fix_criteria}{A named list +(used if \code{method_horizontal = "fix_with_buffer"}). +Names match values of \code{vars} or one of the names can be \code{"texture"} +(in which case the criterion is applied to sand, clay, and silt). +Each element is applied to the variable that corresponds to the name +and is used to determine whether a site has problematic values. +Elements themselves are a named list with two sub-elements +\code{"op"} for the relationship operator, e.g., \code{"<"}, and +\code{"value"} for the value to compare against. See examples.} + +\item{buffer_m}{A numeric value. The radius of a buffer around each point +from which to extract cell values and across which \code{fun} is applied. +Set to \code{NULL} to extract \code{SOLUS100} gridcell values at point locations.} + +\item{fun}{A function or a named list containing functions +(used if \code{method_horizontal = "fix_with_buffer"}). +Names match values of \code{vars} or one of the names can be \code{"texture"} +(in which case the criterion is applied to sand, clay, and silt). +The function(s) summarize(s) extracted values if more than one value +is extracted per location (based on \code{buffer_m}).} + +\item{na.rm}{A logical value. Passed to \code{fun}.} + +\item{digits}{An integer value. The number of digits to which soil texture +variables are rounded. Skip rounding if \code{NA} or \code{NULL}.} + +\item{verbose}{A logical value.} +} +\description{ +Extract soil information from the \code{SOLUS100} soil dataset +for \pkg{SOILWAT2} applications +} +\section{Notes}{ + A local copy of \code{SOLUS100} is required. The function +\code{\link[=download_SOLUS100]{download_SOLUS100()}} can be used to download \code{SOLUS100} files. +} + +\examples{ +\dontrun{ +if (curl::has_internet()) { +path_solus100 <- tempdir() +req_vars <- c("resdept_cm", "sandtotal") +req_depths <- 0 + +## Download data +fns_solus100 <- rSW2exter::download_SOLUS100( + path = path_solus100, + vars = req_vars, + depths = req_depths +) + +## Check that we have SOLUS100 data +has_SOLUS100 <- isTRUE(all( + check_SOLUS100( + path = path_solus100, + vars = req_vars, + depths = req_depths + ) +)) + +if (has_SOLUS100) { + locations <- matrix( + data = c(-120.1286878, -111.8511136, 39.8182913, 36.9047396), + nrow = 2 + ) + + ## Extract gridcell values at point locations + res <- extract_soils_SOLUS100( + x = locations, + vars = req_vars, + depths = req_depths, + path = path_solus100 + ) +} + +# Clean up example +unlink(file.path(path_solus100, fns_solus100)) +} +} + +} +\references{ +Nauman, T. 2024. +Data from: Soil Landscapes of the United States 100-meter (\code{SOLUS100}) +soil property maps project repository. Ag Data Commons. +\url{https://doi.org/10.15482/USDA.ADC/25033856.V1}. +} +\seealso{ +\code{\link[terra:extract]{terra::extract()}} +} diff --git a/man/fetch_soils_from_SOLUS100.Rd b/man/fetch_soils_from_SOLUS100.Rd new file mode 100644 index 0000000..c11c8c5 --- /dev/null +++ b/man/fetch_soils_from_SOLUS100.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{fetch_soils_from_SOLUS100} +\alias{fetch_soils_from_SOLUS100} +\title{Extract soil information from the \code{SOLUS100} soil dataset} +\usage{ +fetch_soils_from_SOLUS100( + x, + vars, + depths, + stat, + path = ".", + fun = NULL, + na.rm = TRUE, + verbose = FALSE +) +} +\arguments{ +\item{x}{A numerical two-dimensional object +(a \code{matrix}, \code{array}, or \code{data.frame}) +with longitude/X, latitude/Y as columns; +a \code{\link[sp:SpatialPoints-class]{sp::SpatialPoints}} object; or +a \code{\link[terra:SpatVector-class]{terra::SpatVector}} object; or +a \var{sf} object with a point geometry, +i.e., an object with a class \var{sf} or \var{sfc}.} + +\item{vars}{A vector of variable names. See \code{\link[=variables_SOLUS100]{variables_SOLUS100()}}} + +\item{depths}{Soil depths in centimeters from surface. +See \code{\link[=depth_profile_SOLUS100]{depth_profile_SOLUS100()}}} + +\item{stat}{A vector of character strings. See Nauman et al. 2024} + +\item{path}{A character string. The path to the local copy of \code{SOLUS100}.} + +\item{fun}{A function. Summarizing gridcell values if more than one value +is extracted per location. See \code{\link[terra:extract]{terra::extract()}}.} + +\item{na.rm}{A logical value. Passed to \code{fun}.} + +\item{verbose}{A logical value.} +} +\description{ +Extract soil information from the \code{SOLUS100} soil dataset +} +\section{Notes}{ + This is a function with minimal functionality; +use \code{\link[=extract_soils_SOLUS100]{extract_soils_SOLUS100()}} for a user-friendly interface. +} + +\references{ +Nauman, T. 2024. +Data from: Soil Landscapes of the United States 100-meter (\code{SOLUS100}) +soil property maps project repository. Ag Data Commons. +\url{https://doi.org/10.15482/USDA.ADC/25033856.V1}. +} diff --git a/man/filenames_SOLUS100.Rd b/man/filenames_SOLUS100.Rd new file mode 100644 index 0000000..55d1361 --- /dev/null +++ b/man/filenames_SOLUS100.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{filenames_SOLUS100} +\alias{filenames_SOLUS100} +\title{Compose a file name of \code{SOLUS100}} +\usage{ +filenames_SOLUS100(vars, depths, stat) +} +\arguments{ +\item{vars}{A vector of variable names. See \code{\link[=variables_SOLUS100]{variables_SOLUS100()}}} + +\item{depths}{Soil depths in centimeters from surface. +See \code{\link[=depth_profile_SOLUS100]{depth_profile_SOLUS100()}}} + +\item{stat}{A vector of character strings. See Nauman et al. 2024} +} +\value{ +A vector of file names. +} +\description{ +Compose a file name of \code{SOLUS100} +} diff --git a/man/variables_SOLUS100.Rd b/man/variables_SOLUS100.Rd new file mode 100644 index 0000000..aac5708 --- /dev/null +++ b/man/variables_SOLUS100.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/extract_soils_NRCS_SOLUS100.R +\name{variables_SOLUS100} +\alias{variables_SOLUS100} +\title{List of variables available from \code{SOLUS100}} +\usage{ +variables_SOLUS100() +} +\description{ +List of variables available from \code{SOLUS100} +} diff --git a/tests/testthat/test_soils_NRCS_SOLUS100.R b/tests/testthat/test_soils_NRCS_SOLUS100.R new file mode 100644 index 0000000..2cdc53c --- /dev/null +++ b/tests/testthat/test_soils_NRCS_SOLUS100.R @@ -0,0 +1,98 @@ + +test_that("Extract soils from SOLUS100", { + skip_on_ci() + skip_on_cran() + skip_if_offline() + + # path relative to rSW2exter/tests/testthat/ + path_solus100 <- file.path("..", "test_data", "SOLUS100") + dir.create(path_solus100, recursive = TRUE, showWarnings = FALSE) + + vars_solus100 <- c("resdept_cm", "sandtotal", "silttotal", "claytotal") + req_depths <- c(0, 5, 150) + requested_layer_depths <- c(5, 15, 30, 100, 150, 200) + + ## Download data (if needed) + fns_solus100 <- download_SOLUS100( + path = path_solus100, + vars = vars_solus100, + depths = req_depths + ) + + ## Check that we have SOLUS100 data + has_SOLUS100 <- isTRUE(all( + check_SOLUS100( + path = path_solus100, + vars = vars_solus100, + depths = req_depths + ) + )) + + expect_true(has_SOLUS100) + + locations <- matrix( + data = c(-120.1286878, -111.8511136, 39.8182913, 36.9047396), + nrow = 2 + ) + + ## k == 1: extract values at surface + ## k == 2: extract all depth values (which allows to interpolate by layer) + for (k in 1:2) { + kids <- if (k == 1L) 1L else seq_along(req_depths) + + ## Extract gridcell values at point locations + res1 <- extract_soils_SOLUS100( + x = locations, + vars = vars_solus100, + depths = req_depths[kids], + path = path_solus100 + ) + + expect_named(res1, c("ref", "table_depths", "table_texture")) + if (k == 1L) { + expect_true( + rSW2data::check_texture_table( + table_texture = res1[["table_texture"]], + n_layers = res1[["table_depths"]][, "N_horizons"] + )[["checks_passed"]] + ) + } + + ## Extract gridcell values + ## * use 700-m buffer at sites with bad values + ## * interpolate vertically by layers + res2 <- extract_soils_SOLUS100( + x = locations, + vars = vars_solus100, + depths = req_depths[kids], + requested_layer_depths = requested_layer_depths, + path = path_solus100, + method_vertical = if (length(kids) > 1L) { + "interpolate_by_layer" + } else { + "asis" + }, + method_horizontal = "fix_with_buffer", + fix_criteria = list( + dbovendry = list(op = "<", value = 0.6), + texture = list(op = "<", value = 50) + ), + buffer_m = 700, + fun = list( + dbovendry = function(x, na.rm = TRUE) median(x[x > 0.6], na.rm = na.rm), + texture = median + ), + na.rm = TRUE, + digits = 3L + ) + + + expect_named(res2, c("ref", "table_depths", "table_texture")) + expect_true( + rSW2data::check_texture_table( + table_texture = res2[["table_texture"]], + n_layers = res2[["table_depths"]][, "N_horizons"] + )[["checks_passed"]] + ) + } +})