-
Notifications
You must be signed in to change notification settings - Fork 273
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?
Conversation
| # Keep other soil data, mark fragments as explicitly missing | ||
| # complete.cases() will filter these out later | ||
| PEcAn.logger::logger.info( | ||
| paste("Fragment data unavailable for depth", top_depth, "-", bottom_depth, |
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.
Note that the logger has its own internal pasting approach. See output of PEcAn.logger:::logger.message.
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.
thanks for the info!
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.
This PR implements the features specified in #3609. Please check comments and suggestions, then will be ready to merge, thanks!
| #' @author Hamze Dokoohaki, Akash | ||
| #' @export | ||
| #' | ||
| extract_soil_gssurgo <- function(outdir, lat, lon, size=1, grid_size=3, grid_spacing=100, depths=c(0.15,0.30,0.60)){ |
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.
With the change back to a set distance instead of a grid, I think it would also make sense to revert the grid_size and grid_spacing parameters back to something more like the radius that was here before #3534.
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.
This can also greatly simplify aoi setup -- lines 30 through 42 below could reduce to
aoi <- data.frame(lon = lon, lat = lat) |>
terra::vect(crs = "epsg:4326") |>
terra::buffer(radius)
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.
Followup thought: I'd also love to have support for passing in a custom area of interest so that I could retrieve e.g. soil properties for one whole field instead of a fixed radius/grid that approximates it. I think the change above would make that pretty easy -- if the user provides a lat/lon then construct AOI as shown here, if they provide a polygon then pass that straight into mukey.wcs.
Does the following (!untested!) design make sense to you?
| extract_soil_gssurgo <- function(outdir, lat, lon, size=1, grid_size=3, grid_spacing=100, depths=c(0.15,0.30,0.60)){ | |
| extract_soil_gssurgo <- function(outdir, lat, lon, aoi = NULL, size = 1, radius = 500, depths=c(0.15, 0.30, 0.60)) { | |
| if (missing(aoi)) { | |
| aoi <- data.frame(lon = lon, lat = lat) |> | |
| terra::vect(crs = "epsg:4326") |> | |
| terra::buffer(radius) | |
| } |
... along with deletion of current lines 30 to 42 below?
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.
i don't really have a strong preference here radius vs grid. Switching to radius makes more sense since we are no longer doing grid sampling (single fetchSDA call)
implemented changes, thanks!
| # all size classes: 2-75mm (pebbles), 75-250mm (cobbles), 250-600mm (stones), and >600mm (boulders). | ||
| # plus component weighting needed for aggregation | ||
| sda_data <- tryCatch({ | ||
| soilDB::fetchSDA( |
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.
Every call to fetchSDA returns all horizons from all components in the requested map units, so this does not need to be inside the loop over depths.
Also, in my local testing it seems all(c("sandtotal_r", "silttotal_r", "claytotal_r", "om_r", "dbthirdbar_r") %in% names(sda_data)) is TRUE, so it may make sense to skip the get_SDA_property call entirely and work just from this result.
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.
I have implemented both suggestions in the recent commit, now fetchSDA retrieves all horizons once, then filters by depth in the loop - much more efficient for complex query
| soc_sd <- stats::sd(soc_mean, na.rm = TRUE) | ||
| n_depths <- length(soc_mean) | ||
|
|
||
| if (n_depths == 1 || is.na(soc_sd) || soc_sd == 0) { |
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.
Why NA when n_depths == 1? I would have expected that 1 layer still means one datapoint to be had
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.
I was visioning back then ,if we have one record it's oboes that the sd become NA.
But good catch!, returning NA for a single observation discards valid data unnecessarily
i have update code to :
if (is.na(soc_sd) || soc_sd == 0) {
# No variability - use measured value (all ensemble members identical)
simulated_soc <- rep(soc_mean, size)
} else {
# Has variability - sample from gamma distribution
shape <- (soc_mean^2) / (soc_sd^2)
rate <- soc_mean / (soc_sd^2)
simulated_soc <- stats::rgamma(size, shape = shape, rate = rate)
}
single component (n=1) sd = NA all ensemble members will get measured value. Multiple different components (sd > 0) sample from distribution.
make sense now ?
| } | ||
|
|
||
| # calling the query function sending the mapunit keys | ||
| soilprop <- gSSURGO.Query( |
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.
Does the approach used here completely replace gSSURGO.Query? If so we might be able to deprecate it now for removal in a future release -- as far as I know it is mostly (only?) used inside extract_soil_nc.
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.
Yes new approach completely replaces gSSURGO.Query and uses fetchSDA, As far as I can tell, gSSURGO.Query is only called within extract_soil_nc.R (now removed), so user impact should be minimal.
|
|
||
| # let's fit dirichlet for each depth level separately | ||
| simulated.soil.props<-soilprop.new.grouped %>% | ||
| split(list(soilprop.new.grouped$DepthL, soilprop.new.grouped$mukey)) %>% |
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.
@divine7022 can you explain more of the intention here? It looks like removing the split by depth switches this from fitting a texture for each depth layer to fitting one texture across the entire depth profile, which is not what we want.
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.
good question! suspecting this is the point where i was got stuck in with the decision
I reverted back to splitting by both depth AND mukey.
The initial change was an experiment to increase sample size for Dirichlet fitting, and get vertical variability.
Context for why I tried it:
The old code used get_SDA_property which returned one pre-aggregated value per (mukey, depth) - essentially 1 observation per group, with n=1 dirichlet parameter estimation is unstable (requires min ~3-5 samples for reasonable MLE convergence).
The new changes uses single fetchSDA call with component-level aggregation, which preserves multiple components per (mukey, depth).
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.
Thanks for reverting. For future work, changes that fundamentally alter a function's behavior ought to be called out in the PR description, especially if they're experimental -- I didn't notice this until I tried a local test run and wondered why it produced files with all layers identical.
…for custom aoi and switched to single fetchSDA call
This reverts commit 7ca57f6.
dlebauer
left a comment
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.
This looks good to me. Thanks for all of your work on this!
@infotroph it looks like @divine7022 has addressed your suggestions and answered your questions. This seems to be a substantial improvement. Are there any errors that would prevent this from being merged?
infotroph
left a comment
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.
A few more comments here. I haven't finished my re-review yet, but I can already say that given the magnitude of the changes it would be very helpful to include some test cases so we can verify the function is behaving as intended.
| terra::project("epsg:5070") %>% | ||
| terra::buffer(width = radius) %>% | ||
| terra::project("epsg:4326") |
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.
Reprojecting introduces distortion that can be severe, and isn't needed because terra knows how to accurately buffer in meters from a geographic CRS. Better to just pass the latlongs and let it buffer from those.
| terra::project("epsg:5070") %>% | |
| terra::buffer(width = radius) %>% | |
| terra::project("epsg:4326") | |
| terra::buffer(width = radius) |
(for a visual demo of the distortion issue, compare the circular result of data.frame(lat=70, lon = 0) |> vect(crs = "epsg:4326") |> buffer(1000) |> plot() to the very elongated result of data.frame(lat=70, lon = 0) |> vect(crs = "epsg:4326") |> project("epsg:5070") |> buffer(1000) |> project("epsg:4326") |> plot())
|
|
||
| # let's fit dirichlet for each depth level separately | ||
| simulated.soil.props<-soilprop.new.grouped %>% | ||
| split(list(soilprop.new.grouped$DepthL, soilprop.new.grouped$mukey)) %>% |
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.
Thanks for reverting. For future work, changes that fundamentally alter a function's behavior ought to be called out in the PR description, especially if they're experimental -- I didn't notice this until I tried a local test run and wondered why it produced files with all layers identical.
| 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), |
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.
Could treating these as three independent averages cause any problems for Dirichlet fitting below in cases where the horizons we're averaging together have strong discontinuities in texture? I haven't worked through the math yet, but maybe you did already.
(By "discontinuity" I mean cases where, say, a horizon with 30% sand and a horizon with 70% sand get combined into a weighted average of 50%. Since soil mappers very often assign horizon boundaries on the basis of a change in texture, this will be a pretty common occurrence in the database.)
Description
Refactored
extract_soil_gssurgo()to replace point-based WFS queries with raster-based Web Coverage Service (WCS) approach, enabling accurate area-weighted sampling and eliminating spatial coverage gaps.soilDB::mukey.wcs()to extract map unit keys with complete spatial coverage at 30m resolution. This provides accurate area weighting through pixel counts and eliminates spatial coverage gaps.soilDB::get_SDA_property()with "Weighted Average" aggregation method. This retrieves soil properties (sand, silt, clay, organic matter, bulk density) integrated across specified depth ranges with component weighting in a single batch query.soilDB::fetchSDA()to obtain complete rock fragment data (fragvol_r) representing total volume across all size classes: 2-75mm (pebbles), 75-250mm (cobbles), 250-600mm (stones), and >600mm (boulders). Applied proper depth and component weighting for fragments:Motivation and Context
Fixes #3609
Review Time Estimate
Types of changes
Checklist: