-
Notifications
You must be signed in to change notification settings - Fork 313
gSSURGO: Fix spatial sampling and improve data aggregation accuracy #3643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 27 commits
fc03b2d
771d05d
7f09a36
beceada
8bf5f72
0103bbe
a49755d
3d2db80
7ca57f6
0dc560b
62fb9c0
948a362
6221bec
0f181f6
5fa9177
55b4092
c2261b0
6281d78
ae98820
83542ad
11b4886
a1e2ba5
79933d3
1d18769
498689f
3de68fc
147de48
b73927f
da57405
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,3 +134,134 @@ gSSURGO.Query <- function(mukeys, | |
| } | ||
|
|
||
|
|
||
| #' Fetch gSSURGO soil data for an area of interest | ||
| #' | ||
| #' Retrieves soil property data from the USDA gSSURGO database using the soilDB package. | ||
| #' This function performs the data retrieval step separately from ensemble generation, | ||
| #' enabling inspection of raw database values and simpler unit testing. | ||
| #' | ||
| #' @param lat Latitude of center point (optional if aoi provided) | ||
| #' @param lon Longitude of center point (optional if aoi provided) | ||
| #' @param aoi Custom area of interest as sf or terra polygon (optional) | ||
| #' @param radius Buffer radius in meters around lat/lon point (default: 500) | ||
| #' @param depths Soil depth breakpoints in meters, must start with 0 | ||
| #' | ||
| #' @return A list containing: | ||
| #' \item{soilprop}{Data frame with component-level soil properties} | ||
| #' \item{mukey_counts}{Table of mapunit key pixel counts (for area weighting)} | ||
| #' \item{depths_cm}{Depth breakpoints in centimeters} | ||
| #' | ||
| #' @export | ||
| #' @author Akash | ||
| #' @examples | ||
| #' \dontrun{ | ||
| #' result <- gssurgo_fetch_area(lat = 40.1, lon = -88.2) | ||
| #' head(result$soilprop) | ||
| #' } | ||
| gssurgo_fetch_area <- function(lat = NULL, lon = NULL, aoi = NULL, | ||
| radius = 500, depths = c(0, 0.15, 0.30, 0.60)) { | ||
|
|
||
| # Validate inputs | ||
| if (is.null(aoi) && (is.null(lat) || is.null(lon))) { | ||
| PEcAn.logger::logger.severe("Must provide either 'aoi' OR both 'lat' and 'lon'") | ||
| } | ||
|
|
||
| # Create AOI from point + radius if needed | ||
| if (is.null(aoi)) { | ||
| aoi <- data.frame(lon = lon, lat = lat) %>% | ||
| terra::vect(crs = "epsg:4326") %>% | ||
| terra::buffer(width = radius) | ||
| } | ||
|
|
||
| # Validate depths parameter | ||
| if (depths[1] != 0) { | ||
| PEcAn.logger::logger.severe( | ||
| "First depth must be 0. Use depths = c(0, 0.15, 0.30, ...) like hist() breaks." | ||
| ) | ||
| } | ||
|
Comment on lines
+181
to
+186
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per conversation (much) earlier in this PR, this is clarifying and simplifying behavior that already existed. 👍 to that. But! This function is the only PEcAn soil tool I can find that expects top-of-layer depths; the others either don't say or expect bottom-of-layer depth:
See also #3775 |
||
|
|
||
| PEcAn.logger::logger.info("Querying gSSURGO Web Coverage Service for map unit keys") | ||
| mu_raster <- soilDB::mukey.wcs(aoi = aoi, db = 'gSSURGO', res = 30) | ||
|
|
||
| mukey_values <- terra::values(mu_raster) | ||
| mukey_values <- mukey_values[!is.na(mukey_values)] | ||
| mukey_counts <- table(mukey_values) | ||
| mukeys_all <- as.character(names(mukey_counts)) | ||
|
|
||
| if (length(mukeys_all) == 0) { | ||
| PEcAn.logger::logger.severe("No mapunit keys were found for this site.") | ||
| } | ||
|
|
||
| # Fetch complete soil data via soilDB | ||
| sda_data <- tryCatch({ | ||
| soilDB::fetchSDA( | ||
| WHERE = paste0("mukey IN (", paste(mukeys_all, collapse = ","), ")"), | ||
| duplicates = TRUE, | ||
| childs = TRUE, | ||
| nullFragsAreZero = TRUE, | ||
| rmHzErrors = TRUE | ||
| ) | ||
| }, error = function(e) { | ||
| PEcAn.logger::logger.error(paste("Failed to fetch SDA data:", e$message)) | ||
| return(NULL) | ||
| }) | ||
|
|
||
| if (is.null(sda_data)) { | ||
| PEcAn.logger::logger.error("Could not retrieve soil data from SDA") | ||
| return(NULL) | ||
| } | ||
|
|
||
| hz_data <- aqp::horizons(sda_data) | ||
| site_data <- aqp::site(sda_data) | ||
|
|
||
| # Component-level aggregation by depth | ||
| depths_cm <- depths * 100 | ||
| all_soil_data <- list() | ||
|
|
||
| for (i in seq_len(length(depths_cm) - 1)) { | ||
| top_depth <- depths_cm[i] | ||
| bottom_depth <- depths_cm[i + 1] | ||
|
|
||
| depth_hz <- hz_data %>% | ||
| dplyr::filter(hzdept_r < bottom_depth & hzdepb_r > top_depth) | ||
|
|
||
| if (nrow(depth_hz) == 0) next | ||
|
|
||
| component_data <- depth_hz %>% | ||
| dplyr::left_join(site_data[, c("cokey", "comppct_r", "mukey")], by = "cokey") %>% | ||
| dplyr::mutate( | ||
| hz_top_adj = pmax(hzdept_r, top_depth), | ||
| hz_bot_adj = pmin(hzdepb_r, bottom_depth), | ||
| hz_thickness = hz_bot_adj - hz_top_adj | ||
| ) %>% | ||
| dplyr::group_by(mukey, cokey, comppct_r) %>% | ||
| dplyr::summarise( | ||
| sandtotal_r = stats::weighted.mean(sandtotal_r, hz_thickness, na.rm = TRUE), | ||
| silttotal_r = stats::weighted.mean(silttotal_r, hz_thickness, na.rm = TRUE), | ||
| claytotal_r = stats::weighted.mean(claytotal_r, hz_thickness, na.rm = TRUE), | ||
| om_r = stats::weighted.mean(om_r, hz_thickness, na.rm = TRUE), | ||
| dbthirdbar_r = stats::weighted.mean(dbthirdbar_r, hz_thickness, na.rm = TRUE), | ||
| fragvol_r = stats::weighted.mean(fragvol_r, hz_thickness, na.rm = TRUE), | ||
| .groups = "drop" | ||
| ) %>% | ||
| dplyr::mutate( | ||
| tex_sum = sandtotal_r + silttotal_r + claytotal_r, | ||
| sandtotal_r = sandtotal_r / tex_sum * 100, | ||
| silttotal_r = silttotal_r / tex_sum * 100, | ||
| claytotal_r = claytotal_r / tex_sum * 100, | ||
| hzdept_r = top_depth, | ||
| hzdepb_r = bottom_depth | ||
| ) %>% | ||
| dplyr::select(-tex_sum) | ||
|
|
||
| all_soil_data[[i]] <- component_data | ||
| } | ||
|
|
||
| soilprop <- do.call(rbind, all_soil_data) | ||
|
|
||
| return(list( | ||
| soilprop = soilprop, | ||
| mukey_counts = mukey_counts, | ||
| depths_cm = depths_cm | ||
| )) | ||
| } | ||
|
infotroph marked this conversation as resolved.
Outdated
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move up to line 25-ish