From c78c4080a5a42487a3a7d8350e7a04c80dffa907 Mon Sep 17 00:00:00 2001 From: "Andrew G. Brown" Date: Sat, 1 Aug 2020 15:32:32 -0700 Subject: [PATCH] improve unit test coverage #143 --- R/AAAA.R | 5 +- R/Class-SoilProfileCollection.R | 10 +- R/SoilProfileCollection-iterators.R | 153 ++++++++------------ R/SoilProfileCollection-methods.R | 6 +- R/SoilProfileCollection-spatial.R | 10 +- R/addBracket.R | 44 +++--- R/aggregateColor.R | 2 +- R/checkSPC.R | 13 +- R/depthOf.R | 8 +- R/estimatePSCS.R | 78 +++++------ R/estimateSoilDepth.R | 30 ++-- R/evalGenHz.R | 34 +++-- R/getArgillicBounds.R | 12 +- R/getCambicBounds.R | 129 +++++++++-------- R/mollicEpipedon.R | 176 ++++++++++++------------ R/mutate.R | 12 +- R/permute_profile.R | 112 +++++++-------- tests/testthat/test-DT-tbl.R | 37 ++++- tests/testthat/test-SPC-objects.R | 28 +++- tests/testthat/test-acc-unc.R | 22 +++ tests/testthat/test-aggregateColor.R | 9 ++ tests/testthat/test-aqp.R | 8 ++ tests/testthat/test-argillic.R | 38 ++--- tests/testthat/test-cambic.R | 49 +++++++ tests/testthat/test-checkHzDepthLogic.R | 50 ++++--- tests/testthat/test-colorContrast.R | 49 +++---- tests/testthat/test-denormalize.R | 6 +- tests/testthat/test-dplyr-verbs.R | 36 +++++ tests/testthat/test-genhz.R | 75 ++++++++-- tests/testthat/test-glom.R | 116 +++++++++------- tests/testthat/test-mollic.R | 22 +++ tests/testthat/test-plotSPC.R | 103 ++++++++------ tests/testthat/test-profileApply.R | 42 +++++- tests/testthat/test-pscs.R | 51 ++++++- tests/testthat/test-sim.R | 32 ++++- tests/testthat/test-soil-depth.R | 62 ++++++--- 36 files changed, 1038 insertions(+), 631 deletions(-) create mode 100644 tests/testthat/test-acc-unc.R create mode 100644 tests/testthat/test-aqp.R create mode 100644 tests/testthat/test-cambic.R create mode 100644 tests/testthat/test-dplyr-verbs.R create mode 100644 tests/testthat/test-mollic.R diff --git a/R/AAAA.R b/R/AAAA.R index 18d24fc5f..453098087 100644 --- a/R/AAAA.R +++ b/R/AAAA.R @@ -1,9 +1,8 @@ # setup a new environment to store error messages, etc. -aqp.env <- new.env(hash=TRUE, parent = parent.frame()) +aqp.env <- new.env(hash = TRUE, parent = parent.frame()) # register options for later use .onLoad <- function(libname, pkgname) { - options(.aqp.show.n.cols=10) - + options(.aqp.show.n.cols = 10) } diff --git a/R/Class-SoilProfileCollection.R b/R/Class-SoilProfileCollection.R index dc53e6fa3..ec721854d 100644 --- a/R/Class-SoilProfileCollection.R +++ b/R/Class-SoilProfileCollection.R @@ -230,6 +230,7 @@ if(requireNamespace("tibble", quietly = TRUE)) # "allow" NULL for the optional slots if(length(metadata$aqp_hzdesgn) == 0) metadata$aqp_hzdesgn <- "" + if(length(metadata$aqp_hztexcl) == 0) metadata$aqp_hztexcl <- "" @@ -317,8 +318,7 @@ setMethod(f = 'show', hzdesgnname(object)), names(h)) } else { # undefined - idx <- - match(c(idname(object), hzidname(object), horizonDepths(object)), names(h)) + idx <- match(c(idname(object), hzidname(object), horizonDepths(object)), names(h)) } # determine number of columns to show, and index to hz / site data @@ -901,8 +901,8 @@ setReplaceMethod("hzdesgnname", if(length(value)) { # several ways to "reset" the hzdesgnname if((value == "") | is.na(value) | is.null(value)) { - value <- character(0) - message("set horizon designation name column to `character` of length zero") + value <- "" + # message("set horizon designation name column to `character` of length zero") } else if (!(value %in% horizonNames(object))) { stop(paste0("horizon designation name (",value,") not in horizon data"), call.=FALSE) } @@ -953,7 +953,7 @@ setReplaceMethod("hztexclname", signature(object = "SoilProfileCollection"), if(length(value)) { # several ways to "reset" the hzdesgnname if((value == "") | is.na(value) | is.null(value)) { - value <- character(0) + value <- "" #message("set horizon texture class name to `character` of length zero") } else if (! value %in% horizonNames(object)) { stop("horizon texture class name not in horizon data", call.=TRUE) diff --git a/R/SoilProfileCollection-iterators.R b/R/SoilProfileCollection-iterators.R index f8ead9030..00262c3ab 100644 --- a/R/SoilProfileCollection-iterators.R +++ b/R/SoilProfileCollection-iterators.R @@ -1,7 +1,7 @@ # profileApply if (!isGeneric("profileApply")) - setGeneric("profileApply", function(object, FUN, + setGeneric("profileApply", function(object, FUN, simplify = TRUE, frameify = FALSE, chunk.size = 100, @@ -15,7 +15,7 @@ if (!isGeneric("profileApply")) # get profile IDs pIDs <- profile_id(object) - + ## TODO: does this same any time / memory? # pre-allocate list l <- vector(mode = 'list', length = length(pIDs)) @@ -29,41 +29,41 @@ if (!isGeneric("profileApply")) } # optionally simplify - if(simplify) + if(simplify) return(unlist(l)) - + return(l) } #' Iterate over profiles in a SoilProfileCollection -#' +#' #' @name profileApply -#' +#' #' @description Iterate over all profiles in a SoilProfileCollection, calling \code{FUN} on a single-profile SoilProfileCollection for each step. -#' +#' #' @param object a SoilProfileCollection -#' +#' #' @param FUN a function to be applied to each profile within the collection -#' +#' #' @param simplify logical, should the result be simplified to a vector? default: TRUE; see examples -#' +#' #' @param frameify logical, should the result be collapsed into a data.frame? default: FALSE; overrides simplify argument; see examples -#' +#' #' @param chunk.size numeric, size of "chunks" for faster processing of large SoilProfileCollection objects; default: 100 -#' +#' #' @param column.names character, optional character vector to replace frameify-derived column names; should match length of colnames() from FUN result; default: NULL -#' +#' #' @param ... additional arguments passsed to FUN #' #' @return When simplify is TRUE, a vector of length nrow(object) (horizon data) or of length length(object) (site data). When simplify is FALSE, a list is returned. When frameify is TRUE, a data.frame is returned. An attempt is made to identify idname and/or hzidname in the data.frame result, safely ensuring that IDs are preserved to facilitate merging profileApply result downstream. -#' +#' #' @aliases profileApply,SoilProfileCollection-method #' @docType methods #' @rdname profileApply #' @examples -#' +#' #' data(sp1) #' depths(sp1) <- id ~ top + bottom -#' +#' #' # estimate soil depth using horizon designations #' profileApply(sp1, estimateSoilDepth, name='name', top='top', bottom='bottom') #' @@ -71,73 +71,73 @@ if (!isGeneric("profileApply")) #' # scaled = (x - mean(x)) / sd(x) #' sp1$d <- profileApply(sp1, FUN=function(x) round(scale(x$prop), 2)) #' plot(sp1, name='d') -#' +#' #' # compute depth-wise differencing by profile #' # note that our function expects that the column 'prop' exists #' f <- function(x) { c(x$prop[1], diff(x$prop)) } #' sp1$d <- profileApply(sp1, FUN=f) #' plot(sp1, name='d') -#' +#' #' # compute depth-wise cumulative sum by profile #' # note the use of an anonymous function #' sp1$d <- profileApply(sp1, FUN=function(x) cumsum(x$prop)) #' plot(sp1, name='d') -#' +#' #' # compute profile-means, and save to @site #' # there must be some data in @site for this to work #' site(sp1) <- ~ group #' sp1$mean_prop <- profileApply(sp1, FUN=function(x) mean(x$prop, na.rm=TRUE)) -#' +#' #' # re-plot using ranks defined by computed summaries (in @site) #' plot(sp1, plot.order=rank(sp1$mean_prop)) -#' +#' #' ## iterate over profiles, calculate on each horizon, merge into original SPC -#' +#' #' # example data #' data(sp1) -#' +#' #' # promote to SoilProfileCollection #' depths(sp1) <- id ~ top + bottom #' site(sp1) <- ~ group -#' +#' #' # calculate horizon thickness and proportional thickness #' # returns a data.frame result with multiple attributes per horizon #' thicknessFunction <- function(p) { #' hz <- horizons(p) #' depthnames <- horizonDepths(p) -#' res <- data.frame(profile_id(p), hzID(p), +#' res <- data.frame(profile_id(p), hzID(p), #' thk=(hz[[depthnames[[2]]]] - hz[[depthnames[1]]])) #' res$hz_prop <- res$thk / sum(res$thk) #' colnames(res) <- c(idname(p), hzidname(p), 'hz_thickness', 'hz_prop') #' return(res) #' } -#' +#' #' # list output option with simplify=F, list names are profile_id(sp1) #' list.output <- profileApply(sp1, thicknessFunction, simplify = FALSE) #' head(list.output) -#' +#' #' # data.frame output option with frameify=TRUE #' df.output <- profileApply(sp1, thicknessFunction, frameify = TRUE) #' head(df.output) -#' -#' # since df.output contains idname(sp1) and hzidname(sp1), +#' +#' # since df.output contains idname(sp1) and hzidname(sp1), #' # it can safely be merged by a left-join via horizons<- setter #' horizons(sp1) <- df.output -#' +#' #' plot(density(sp1$hz_thickness, na.rm=TRUE), main="Density plot of Horizon Thickness") -#' +#' #' ## iterate over profiles, subsetting horizon data -#' +#' #' # example data #' data(sp1) -#' +#' #' # promote to SoilProfileCollection #' depths(sp1) <- id ~ top + bottom #' site(sp1) <- ~ group -#' +#' #' # make some fake site data related to a depth of some importance #' sp1$dep <- profileApply(sp1, function(i) {round(rnorm(n=1, mean=mean(i$top)))}) -#' +#' #' # custom function for subsetting horizon data, by profile #' # keep horizons with lower boundary < site-level attribute 'dep' #' fun <- function(i) { @@ -150,38 +150,38 @@ if (!isGeneric("profileApply")) #' # return modified SPC #' return(i) #' } -#' +#' #' # list of modified SoilProfileCollection objects #' l <- profileApply(sp1, fun, simplify=FALSE) -#' +#' #' # re-combine list of SoilProfileCollection objects into a single SoilProfileCollection #' sp1.sub <- union(l) -#' +#' #' # graphically check #' par(mfrow=c(2,1), mar=c(0,0,1,0)) #' plot(sp1) #' points(1:length(sp1), sp1$dep, col='red', pch=7) #' plot(sp1.sub) -#' -setMethod(f='profileApply', signature='SoilProfileCollection', function(object, FUN, - simplify = TRUE, +#' +setMethod(f='profileApply', signature='SoilProfileCollection', function(object, FUN, + simplify = TRUE, frameify = FALSE, - chunk.size = 100, - column.names = NULL, + chunk.size = 100, + column.names = NULL, ...) { if(simplify & frameify) { # simplify and frameify are both TRUE -- ignoring simplify argument simplify <- FALSE } - - # total number of profiles we have to iterate over + + # total number of profiles we have to iterate over n <- length(object) o.name <- idname(object) o.hname <- hzidname(object) - + # split the SPC of size n into chunk.size chunks chunk <- sort(1:n %% max(1, round(n / chunk.size))) + 1 - + # we first iterate over list of chunks of size chunk.size, keeping lists smallish # by dividing by a tunable factor -- set as 100 by default # then we iterate through each chunk, calling FUN on each element (profile) @@ -189,73 +189,34 @@ setMethod(f='profileApply', signature='SoilProfileCollection', function(object, res <- do.call('c', lapply(split(1:n, chunk), function(idx) { .profileApply(object[idx,], FUN, simplify, ...) })) - + # return profile IDs if it makes sense for result if(length(res) == length(object)) names(res) <- profile_id(object) - - # return horizon IDs if it makes sense for result - if(!simplify & inherits(res, 'data.frame')) - if(nrow(res) == nrow(object)) - names(res) <- hzID(object) - + # combine a list (one element per profile) into data.frame result if(!simplify & frameify) { - + # make sure the first result is a data.frame (i.e. FUN returns a data.frame) if(is.data.frame(res[[1]])) { - + # make a big data.frame res <- as.data.frame(do.call('rbind', res), stringsAsFactors = FALSE) - + # get ids pid <- profile_id(object) hz.id <- hzID(object) - - # determine if merge of res into @horizon or @site is feasible/reasonable - # if so, we want to keep track of unique site and horizon IDs - - # if hzidname is in big df and all hzID in result are from parent object... - if(o.hname %in% colnames(res) & all(res[[o.hname]] %in% hz.id)) { - - # make a master site/horizon id table (all in SPC) - pid.by.hz <- horizons(object)[[o.name]] - id.df <- data.frame(pid.by.hz, hz.id, stringsAsFactors = FALSE) - colnames(id.df) <- c(o.name, o.hname) - - # warn if some hz IDs are missing in result - if(!all(hz.id %in% res[[o.hname]])) { - warning("frameify found horizon ID (", o.hname, ") in result but some IDs are missing!", call.=FALSE) - } - - # do a left join, filling in any missing idname, hzidname from res with NA - res <- merge(id.df, res, all.x = TRUE, sort=FALSE) - - } else if(o.name %in% colnames(res) & all(res[[o.name]] %in% pid)) { - - # same as above, only for site level summaries (far more common) - id.df <- data.frame(pid, stringsAsFactors = FALSE) - colnames(id.df) <- c(o.name) - - if(!all(pid %in% res[[o.name]])) { - # this shouldn't really happen -- usually a problem with FUN - warning("frameify found site ID (", o.name, ") in result but some IDs are missing!", call.=FALSE) - } - - # do a left join, filling in any missing idname from res with NA - res <- merge(id.df, res, all.x = TRUE, sort=FALSE) - } - - if(!is.null(column.names)) + + if (!is.null(column.names)) colnames(res) <- column.names - + } else { warning("first result is not class `data.frame` and frameify is TRUE. defaulting to list output.", call. = FALSE) } } - + if(simplify) return(unlist(res)) - + return(res) }) diff --git a/R/SoilProfileCollection-methods.R b/R/SoilProfileCollection-methods.R index 670882a95..056ae8f91 100644 --- a/R/SoilProfileCollection-methods.R +++ b/R/SoilProfileCollection-methods.R @@ -483,14 +483,12 @@ setMethod("subsetProfiles", signature(object = "SoilProfileCollection"), # subset using conventional data.frame methods if (!missing(s)) - s.d.sub.IDs <- - subset(s.d, select = id.col, subset = eval(parse(text = s)))[, 1] # convert to vector + s.d.sub.IDs <- subset(s.d, select = id.col, subset = eval(parse(text = s)))[, 1] # convert to vector else s.d.sub.IDs <- NA if (!missing(h)) - h.d.sub.IDs <- - subset(h.d, select = id.col, subset = eval(parse(text = h)))[, 1] # convert to vector + h.d.sub.IDs <- subset(h.d, select = id.col, subset = eval(parse(text = h)))[, 1] # convert to vector else h.d.sub.IDs <- NA diff --git a/R/SoilProfileCollection-spatial.R b/R/SoilProfileCollection-spatial.R index 3c8c5e216..57263ae5f 100644 --- a/R/SoilProfileCollection-spatial.R +++ b/R/SoilProfileCollection-spatial.R @@ -82,17 +82,15 @@ setReplaceMethod("coordinates", "SoilProfileCollection", # assign to sp slot # note that this will clobber any existing spatial data - object@sp <- SpatialPoints(coords=mf) + object@sp <- SpatialPoints(coords = mf) # remove coordinates from source data # note that mf is a matrix, so we need to access the colnames differently coord_names <- dimnames(mf)[[2]] - idx <- match(coord_names, siteNames(object)) + sn <- siteNames(object) - # remove the named site data from site_data - # TODO we should use a proper setter! - # bug fix c/o Jose Padarian: drop=FALSE - object@site <- site(object)[, -idx, drop=FALSE] + # @site minus coordinates "promoted" to @sp + object@site <- .data.frame.j(object@site, sn[!sn %in% coord_names], aqp_df_class(object)) # done return(object) diff --git a/R/addBracket.R b/R/addBracket.R index 4e35d0e97..1e5cd7373 100644 --- a/R/addBracket.R +++ b/R/addBracket.R @@ -1,19 +1,19 @@ ## TODO: still not completely generalized -# annotate elements from @diagnostic with brackets +# annotate elements from @diagnostic with brackets # mostly a helper function for addBracket() addDiagnosticBracket <- function(s, kind, feature='featkind', top='featdept', bottom='featdepb', ...) { - + # extract diagnostic horizon information # note: the idname is already present in `d` d <- diagnostic_hz(s) d <- d[which(d[[feature]] == kind), ] - + # rename columns so that addBracket() can find top/bottom depths nm <- names(d) nm[which(nm == top)] <- 'top' nm[which(nm == bottom)] <- 'bottom' names(d) <- nm - + # there may be no matching features, in that case issue a message and do nothing if(nrow(d) < 1) { message('no matching features found') @@ -22,7 +22,7 @@ addDiagnosticBracket <- function(s, kind, feature='featkind', top='featdept', bo # sorting is done via matching idname to plot order of idname addBracket(d, ...) } - + } @@ -34,60 +34,60 @@ addDiagnosticBracket <- function(s, kind, feature='featkind', top='featdept', bo # tick.length: bracket tick length # offset: left-hand offset from profile center addBracket <- function(x, label.cex=0.75, tick.length=0.05, arrow.length=0.05, offset=-0.3, missing.bottom.depth=NULL, ...) { - + # get plotting details from aqp environment lsp <- get('last_spc_plot', envir=aqp.env) depth.offset <- lsp$y.offset sf <- lsp$scaling.factor - + # test for required columns: # lsp$idname # top # bottom if(is.null(x$top) | is.null(x$bottom) | is.null(x[[lsp$idname]])) stop('required columns missing', call. = FALSE) - + # test for > 0 rows if(nrow(x) < 1) { warning('no rows') return(FALSE) } - + # test for all missing top depths if(all(is.na(x$top))) stop('no top depths supplied') - + # test for missing label column do.label <- FALSE if(! is.null(x$label)) do.label <- TRUE - + # test for all bottom depths missing no.bottom <- FALSE if(all(is.na(x$bottom))) no.bottom <- TRUE - + # if not specified, use the max depth in the last plot if(is.null(missing.bottom.depth)) { missing.bottom.depth <- lsp$max.depth } - + # apply scale and offset to missing bottom depth missing.bottom.depth <- (missing.bottom.depth * sf) + depth.offset - + # re-order rows of x, according to IDs idx <- match(lsp$pIDs, x[[lsp$idname]]) x <- x[idx, ] - + # determine horizon depths in current setting # depth_prime = (depth * scaling factor) + y.offset top <- (x$top * sf) + depth.offset bottom <- (x$bottom * sf) + depth.offset - + ## x-coordinates # 2019-07-15: using relative position x.base <- lsp$x0 - + # normal case: both top and bottom defined if(!missing(top) & !missing(bottom)) { # x-positions @@ -100,7 +100,7 @@ addBracket <- function(x, label.cex=0.75, tick.length=0.05, arrow.length=0.05, o # vertical bar segments(x.1, top, x.1, bottom, lend=2, ...) } - + # missing bottom: replace bottom tick with arrow head if(no.bottom) { # x-positions @@ -111,15 +111,15 @@ addBracket <- function(x, label.cex=0.75, tick.length=0.05, arrow.length=0.05, o # vertical bar is now an arrow arrows(x.1, top, x.1, top + missing.bottom.depth, length=arrow.length, lend=2, ...) } - + # optionally plot label - if(do.label){ - if(no.bottom) + if (do.label) { + if (no.bottom) bottom <- rep(missing.bottom.depth, times=length(bottom)) # add labels text(x.1 - 0.05, (top + bottom)/2, x$label, srt=90, cex=label.cex, pos=3) } - + } diff --git a/R/aggregateColor.R b/R/aggregateColor.R index 5cb9f4f32..f07a8bbd2 100644 --- a/R/aggregateColor.R +++ b/R/aggregateColor.R @@ -12,7 +12,7 @@ aggregateColor <- function(x, groups='genhz', col='soil_color', colorSpace = 'CI k <- round(k) # sanity check, need this for color distance eval - if(!requireNamespace('farver', quietly = TRUE)) + if (!requireNamespace('farver', quietly = TRUE)) stop('please install the `farver` package.', call.=FALSE) if(is.na(k)) { diff --git a/R/checkSPC.R b/R/checkSPC.R index a51da4625..5b3ff08eb 100644 --- a/R/checkSPC.R +++ b/R/checkSPC.R @@ -1,22 +1,21 @@ -# test for valid SPC, based on presence / absense of slots as compared to +# test for valid SPC, based on presence / absense of slots as compared to # class prototype # likely only used between major versions of aqp where internal structure of SPC has changed checkSPC <- function(x) { - + # get slot names from prototype sn <- slotNames(x) - + # test for all slots in the prototype s.test <- sapply(sn, function(i) .hasSlot(x, name=i)) - + + res <- FALSE # a valid object will have all slots present if(all(s.test)) { res <- TRUE - } else { - res <- FALSE } - + return(res) } diff --git a/R/depthOf.R b/R/depthOf.R index 73572f312..800679cba 100644 --- a/R/depthOf.R +++ b/R/depthOf.R @@ -59,15 +59,15 @@ depthOf <- function(p, pattern, top = TRUE, hzdesgn = guessHzDesgnName(p), no.contact.depth = NULL, no.contact.assigned = NA) { - if(!inherits(p, 'SoilProfileCollection')) + if (!inherits(p, 'SoilProfileCollection') | length(p) != 1) stop("`p` must be a SoilProfileCollection containing one profile") hznames <- horizonNames(p) # if the user has not specified a column containing horizon designations - if(!hzdesgn %in% hznames) { + if (!hzdesgn %in% hznames) { hzdesgn <- guessHzDesgnName(p) - if(!hzdesgn %in% hznames) { + if (!hzdesgn %in% hznames) { stop("depth estimation relies on a column containing horizon designations") } } @@ -76,7 +76,7 @@ depthOf <- function(p, pattern, top = TRUE, hzdesgn = guessHzDesgnName(p), hz.match <- horizons(p)[grepl(pattern, p[[hzdesgn]]),] # if no horizons match, return `no.contact.assigned` - if(nrow(hz.match) == 0) { + if (nrow(hz.match) == 0) { return(no.contact.assigned) } diff --git a/R/estimatePSCS.R b/R/estimatePSCS.R index 5d22e0846..3d3712cc6 100644 --- a/R/estimatePSCS.R +++ b/R/estimatePSCS.R @@ -7,25 +7,24 @@ estimatePSCS = function(p, hzdesgn = "hzname", clay.attr = "clay", hz.depths <- horizonDepths(p) attr.len <- unlist(lapply(c(hzdesgn, clay.attr, texcl.attr), length)) - if(any(attr.len > 1)) + if (any(attr.len > 1)) stop("horizon designation, clay attribute or texture class attribute must have length 1") - if(is.null(hzdesgn) | (!hzdesgn %in% horizonNames(p))) { + if (is.null(hzdesgn) | (!hzdesgn %in% horizonNames(p))) { hzdesgn <- guessHzDesgnName(p) - if(is.na(hzdesgn)) + if (hzdesgn == "") stop("horizon designation column not correctly specified") } - if(is.null(clay.attr) | (!clay.attr %in% horizonNames(p))) { - print((clay.attr)) - clay.attr <- guessHzAttrName(p, attr="clay", optional=c("total","_r")) - if(is.na(clay.attr)) + if (is.null(clay.attr) | (!clay.attr %in% horizonNames(p))) { + clay.attr <- guessHzAttrName(p, attr = "clay", optional = c("total","_r")) + if (clay.attr == "") stop("horizon clay content column not correctly specified") } - if(is.null(texcl.attr) | (!texcl.attr %in% horizonNames(p))) { + if (is.null(texcl.attr) | (!texcl.attr %in% horizonNames(p))) { texcl.attr <- guessHzTexClName(p) - if(is.na(texcl.attr)) + if (texcl.attr == "") stop("horizon texture class column not correctly specified") } @@ -40,19 +39,19 @@ estimatePSCS = function(p, hzdesgn = "hzname", clay.attr = "clay", default_b <- 100 # Key part A (soils with restriction in shallow depth) - if(soildepth <= 36) { + if (soildepth <= 36) { default_t <- 0 default_b <- soildepth shallow_flag <- TRUE } # Key part B (Andisols) - if(tax_order_field %in% siteNames(p)) { - if(length(site(p)[[tax_order_field]])) { - if(!is.na(site(p)[[tax_order_field]])) { - if(site(p)[[tax_order_field]] == "andisols") { + if (tax_order_field %in% siteNames(p)) { + if (length(site(p)[[tax_order_field]])) { + if (!is.na(site(p)[[tax_order_field]])) { + if (all(grepl("[Aa]ndisols", site(p)[[tax_order_field]]))) { default_t <- 0 - default_b <-100 + default_b <- 100 andisols_flag <- TRUE } } @@ -61,54 +60,55 @@ estimatePSCS = function(p, hzdesgn = "hzname", clay.attr = "clay", # Adjust PSCS range downward if organic soil material is present at surface (i.e. mineral soil surface depth > 0) odepth <- getMineralSoilSurfaceDepth(p, hzdesgn) - if(odepth > 0) { - default_t = default_t + odepth - if(default_b != soildepth) - default_b = default_b + odepth + if (odepth > 0) { + default_t <- default_t + odepth + if (default_b != soildepth) + default_b <- default_b + odepth } # Key parts C and E (has argillic/kandic/natric WITHIN 100CM) - if(!andisols_flag) { + if (!andisols_flag) { argillic_bounds <- getArgillicBounds(p, clay.attr = clay.attr, texcl.attr = texcl.attr, hzdesgn = hzdesgn, - bottom.pattern = bottom.pattern, ...) - if(!any(is.na(argillic_bounds))) { - if(argillic_bounds[1] < 100) { + bottom.pattern = bottom.pattern, + ...) + + if (!any(is.na(argillic_bounds))) { + if (argillic_bounds[1] < 100) { default_t <- argillic_bounds[1] + # Part C - argillic near surface - if(argillic_bounds[1] <= 100) { + if (argillic_bounds[1] <= 100) { # TODO: check arenic and grossarenic subgroups, fragipan depths, strongly contrasting PSCs... should work fine for CA630 though - if(argillic_bounds[2] - argillic_bounds[1] <= 50) + if (argillic_bounds[2] - argillic_bounds[1] <= 50) { default_b <- argillic_bounds[2] - else + } else { default_b <- argillic_bounds[1] + 50 - } else if(argillic_bounds[2] <= 25) { - default_b = 100 + } + if (argillic_bounds[2] <= 25) { + default_b <- 100 + } } } } } # Adjust PSCS top depth to bottom of plow layer (if appropriate) - plow_layer_depth = getPlowLayerDepth(p, hzdesgn) - if(plow_layer_depth) - if(plow_layer_depth >= 25 + odepth) - default_t = plow_layer_depth + plow_layer_depth <- getPlowLayerDepth(p, hzdesgn) + if (plow_layer_depth) + if (plow_layer_depth >= 25 + odepth) + default_t <- plow_layer_depth # Adjust PSCS top depth to mineral soil surface for soils <36cm to restriction - if(shallow_flag & default_t != 0) { + if (shallow_flag & default_t != 0) { default_t <- odepth } # Adjust PSCS bottom depth to restriction depth, if appropriate - if(soildepth < default_b) { #truncate to restriction - default_b = soildepth + if (soildepth < default_b) { #truncate to restriction + default_b <- soildepth } return(as.numeric(c(default_t, default_b))) } - - - - diff --git a/R/estimateSoilDepth.R b/R/estimateSoilDepth.R index 43f502da4..0deba39e6 100644 --- a/R/estimateSoilDepth.R +++ b/R/estimateSoilDepth.R @@ -1,27 +1,27 @@ # get soil depth based on morphology estimateSoilDepth <- function(f, name='hzname', top='hzdept', bottom='hzdepb', p='Cr|R|Cd', no.contact.depth=NULL, no.contact.assigned=NULL) { - + # sanity check: this function will only operate on an SPC - if(! inherits(f, 'SoilProfileCollection')) + if(!inherits(f, 'SoilProfileCollection') | length(f) != 1) stop('`f` must be a SoilProfileCollection containing one profile') - + depthcols <- horizonDepths(f) - + # ease removal of attribute name arguments -- deprecate them later # for now, just fix em if the defaults dont match the depthcols slot if(any(!(c(top, bottom) %in% horizonNames(f)))) { top <- depthcols[1] bottom <- depthcols[2] } - + # sanity check: this function works on a single soil profile if(length(f) > 1) stop('`f` can contain only one profile, see manual page for details') - + # if name is not in horizons, look if it is set in hzdesgncol hznames <- horizonNames(f) - + # if the user has not specified a column containing horizon designations if(!name %in% hznames) { name <- guessHzDesgnName(f) @@ -29,35 +29,35 @@ estimateSoilDepth <- function(f, name='hzname', top='hzdept', bottom='hzdepb', p stop("soil depth estimation relies on a column containing horizon designations", call.=FALSE) } } - + # extract horizons h <- horizons(f) - + # extract possible contact contact.idx <- grep(p, h[[name]], ignore.case=TRUE) # everything else no.contact.idx <- grep(p, h[[name]], ignore.case=TRUE, invert=TRUE) - + # no contact defined, use deepest hz bottom depth if(length(contact.idx) < 1) { d <- max(h[[bottom]][no.contact.idx], na.rm=TRUE) - + # is there a user-specified depth at which we assume a standard depth? if(!missing(no.contact.depth) & !missing(no.contact.assigned)) { if(d >= no.contact.depth & !is.null(no.contact.assigned)) res <- no.contact.assigned - else + else res <- d } - + # otherwise use depth of deepest horizon else res <- d } - + # contact defined else res <- min(h[[top]][contact.idx], na.rm=TRUE) - + return(res) } diff --git a/R/evalGenHz.R b/R/evalGenHz.R index e4aef0a3a..56a3526f8 100644 --- a/R/evalGenHz.R +++ b/R/evalGenHz.R @@ -10,34 +10,39 @@ evalGenHZ <- function(obj, genhz, vars, non.matching.code='not-used', stand=TRUE # extract site / horizons as DF h <- as(obj, 'data.frame') + # genhz may have its own levels set, but if not a factor, make one + if (!is.factor(h[[genhz]])) + h[[genhz]] <- factor(h[[genhz]]) + # make an index to complete data no.na.idx <- which(complete.cases(h[, vars])) # test for duplicate data # unique IDs are based on a concatenation of variables used... digest would be safer h.to.test <- h[no.na.idx, c(idname(obj), vars)] - h.to.test$id <- apply(h.to.test[, vars], 1, function(i) paste0(i, collapse = '|')) + h.to.test$id <- apply(h.to.test[, vars, drop = FALSE], 1, function(i) paste0(i, collapse = '|')) dupe.names <- names(which(table(h.to.test$id) > 1)) dupe.rows <- h.to.test[which(h.to.test$id %in% dupe.names), ] - dupe.ids <- paste0(unique(dupe.rows[[idname(obj)]]), collapse=', ') - warning(paste0('duplicate data associated with pedons: ', dupe.ids), call. = FALSE) + dupe.ids <- paste0(unique(dupe.rows[[idname(obj)]]), collapse = ', ') + if (dupe.ids != "") + warning(paste0('duplicate data associated with pedons: ', dupe.ids), call. = FALSE) # compute pair-wise dissimilarities using our variables of interest - d <- daisy(h[no.na.idx, vars], stand=stand, metric=metric) + d <- daisy(h[no.na.idx, vars, drop = FALSE], stand = stand, metric = metric) # fudge-factor in case of duplicate data (0s in the dissimilarity matrix) dupe.idx <- which(d < 1e-8) - if(length(dupe.idx) > 0) { + if (length(dupe.idx) > 0) { fudge <- min(d[which(d > 0)]) / 100 d[dupe.idx] <- fudge } # perform non-metric MDS of dissimilarity matrix - mds <- MASS::isoMDS(d, trace=trace) + mds <- MASS::isoMDS(d, trace = trace) # compute silhouette widths after removing not-used genhz class - sil.idx <- which(complete.cases(h[, vars]) & h[[genhz]] != non.matching.code) - d.sil <- daisy(h[sil.idx, vars], stand=stand) + sil.idx <- which(complete.cases(h[, vars, drop = FALSE]) & h[[genhz]] != non.matching.code) + d.sil <- daisy(h[sil.idx, vars, drop = FALSE], stand=stand) sil <- silhouette(as.numeric(h[[genhz]])[sil.idx], d.sil) # add new columns @@ -53,18 +58,21 @@ evalGenHZ <- function(obj, genhz, vars, non.matching.code='not-used', stand=TRUE h$neighbor[sil.idx] <- levels(h[[genhz]])[sil[, 2]] # melt into long form - m <- melt(h, id.vars=genhz, measure.vars=c(vars, 'sil.width')) + m <- melt(h, id.vars = genhz, measure.vars = c(vars, 'sil.width')) # compute group-wise summaries-- note that text is returned m.summary <- ddply(m, c(genhz, 'variable'), function(i) { - stats <- format(paste0(round(mean(i$value, na.rm=TRUE), 2), ' (' , sd=round(sd(i$value, na.rm=TRUE), 2), ')'), justify='right') - return(data.frame(stats=stats)) + stats <- format(paste0(round(mean(i$value, na.rm=TRUE), 2), ' (' , + sd = round(sd(i$value, na.rm=TRUE), 2), ')'), + justify = 'right') + return(data.frame(stats = stats)) }) fm <- paste0(genhz, ' ~ variable') - genhz.stats <- cast(m.summary, fm, value='stats') + genhz.stats <- cast(m.summary, fm, value = 'stats') # composite into a list - res <- list(horizons=h[, c('mds.1', 'mds.2', 'sil.width', 'neighbor')], stats=genhz.stats, dist=d) + res <- list(horizons = h[, c('mds.1', 'mds.2', 'sil.width', 'neighbor')], + stats = genhz.stats, dist = d) return(res) } diff --git a/R/getArgillicBounds.R b/R/getArgillicBounds.R index 9933e8001..a79aa84d3 100644 --- a/R/getArgillicBounds.R +++ b/R/getArgillicBounds.R @@ -142,7 +142,7 @@ getArgillicBounds <- function(p, p = bottom.pattern) # if the last horizon with a t is below the contact (Crt or Rt) or some other weird reason - if(soil.depth < depth.last) { + if (soil.depth < depth.last) { # return the soil depth to contact lower.bound <- soil.depth @@ -163,11 +163,15 @@ getArgillicBounds <- function(p, lower.bound <- NA } - if(!is.finite(lower.bound)) + if (!is.finite(lower.bound)) lower.bound <- NA + if (is.na(upper.bound)) + return(c(NA, NA)) + bdepths <- glom(p, 0, upper.bound, df = TRUE)[[depthcol[2]]] - if(all(!is.na(c(upper.bound, lower.bound)))) { + if (all(!is.na(c(upper.bound, lower.bound)))) { + # if argi bounds are found check that minimum thickness requirements are met min.thickness <- max(7.5, max(bdepths, na.rm = TRUE) / 10) @@ -188,7 +192,7 @@ getArgillicBounds <- function(p, if(!as.list) return(c(upper.bound, lower.bound)) - res <- list(ubound=as.numeric(upper.bound), lbound=as.numeric(lower.bound)) + res <- list(ubound = as.numeric(upper.bound), lbound = as.numeric(lower.bound)) return(res) } diff --git a/R/getCambicBounds.R b/R/getCambicBounds.R index b4120c5e2..a2fd8e966 100644 --- a/R/getCambicBounds.R +++ b/R/getCambicBounds.R @@ -3,8 +3,8 @@ # (defined either by depth interval or getArgillicBounds) #' Find all intervals that are potentially part of a Cambic horizon -#' @description Find all intervals that are potentially part of a Cambic horizon excluding those that are part of an argillic horizon (defined either by depth interval or \code{getArgillicBounds()}). -#' +#' @description Find all intervals that are potentially part of a Cambic horizon excluding those that are part of an argillic horizon (defined either by depth interval or \code{getArgillicBounds()}). +#' #' There may be multiple cambic horizons (indexes) in a profile. Each cambic index has a top and bottom depth associated: cambic_top and cambic_bottom. This result is designed to be used for single profiles, or with \code{profileApply(..., frameify = TRUE)} #' #' @param p A single-profile SoilProfileCollection @@ -18,121 +18,128 @@ #' @param m_chroma Column name containing moist crhoma. Default: m_chroma #' #' @return A \code{data.frame} containing profile, cambic indexes, along with top and bottom depths. -#' +#' #' @author Andrew G. Brown -#' +#' #' @export #' -#' @examples +#' @examples #' # construct a fake profile #' spc <- data.frame(id=1, taxsubgrp = "Lithic Haploxerepts", #' hzname = c("A","AB","Bw","BC","R"), -#' hzdept = c(0, 20, 32, 42, 49), -#' hzdepb = c(20, 32, 42, 49, 200), +#' hzdept = c(0, 20, 32, 42, 49), +#' hzdepb = c(20, 32, 42, 49, 200), #' clay = c(19, 22, 22, 21, NA), #' texcl = c("l","l","l", "l","br"), #' d_value = c(5, 5, 5, 6, NA), #' m_value = c(2.5, 3, 3, 4, NA), #' m_chroma = c(2, 3, 4, 4, NA)) -#' +#' #' # promote to SoilProfileCollection #' depths(spc) <- id ~ hzdept + hzdepb #' hzdesgnname(spc) <- 'hzname' #' hztexclname(spc) <- 'texcl' -#' +#' #' # print results in table #' getCambicBounds(spc) #' -getCambicBounds <- function(p, - hzdesgn = guessHzDesgnName(p), - texcl.attr = guessHzTexClName(p), - clay.attr = guessHzAttrName(p, attr = 'clay', - c("total", "_r")), +getCambicBounds <- function(p, + hzdesgn = guessHzDesgnName(p), + texcl.attr = guessHzTexClName(p), + clay.attr = guessHzAttrName(p, attr = 'clay', c("total", "_r")), argi_bounds = NULL, - d_value = "d_value", - m_value = "m_value", + d_value = "d_value", + m_value = "m_value", m_chroma = "m_chroma", ...) { - + # construct data.frame result for no-cambic-found (NA) - empty_frame <- data.frame(id=profile_id(p), - cambic_id=NA, cambic_top=NA, cambic_bottom=NA) - + empty_frame <- data.frame(id = character(0), + cambic_id = numeric(0), + cambic_top = numeric(0), + cambic_bottom = numeric(0)) + empty_frame_names <- names(empty_frame) empty_frame_names[1] <- idname(p) names(empty_frame) <- empty_frame_names - + depths <- horizonDepths(p) - - if(is.null(argi_bounds)) { + + if (is.null(argi_bounds)) { argi_bounds <- getArgillicBounds(p, hzdesgn, clay.attr, texcl.attr, ...) } - - cambic_top <- minDepthOf(p, pattern = "B", - no.contact.assigned = NA) - cambic_bottom <- maxDepthOf(p, pattern = "B", top = FALSE, - no.contact.assigned = NA) - - if(any(is.na(cambic_top), is.na(cambic_bottom))) { + + cambic_top <- minDepthOf(p, pattern = "B", no.contact.assigned = NA) + cambic_bottom <- maxDepthOf(p, pattern = "B", top = FALSE, no.contact.assigned = NA) + + if (any(is.na(cambic_top), is.na(cambic_bottom))) { return(empty_frame) } - - cambic <- glom(p, cambic_top, cambic_bottom, truncate=TRUE) - - if(all(is.finite(argi_bounds))) { - if(all(c(cambic_bottom <= argi_bounds[2], cambic_top >= argi_bounds[1]))) { - return(empty_frame) + + cambic <- glom(p, cambic_top, cambic_bottom, truncate = TRUE) + + if (all(is.finite(argi_bounds))) { + if (all(c(cambic_bottom <= argi_bounds[2], cambic_top >= argi_bounds[1]))) { + return(empty_frame) } - # if an argillic is presnt, remove with glom truncate+invert - non.argillic <- suppressWarnings(glom(cambic, argi_bounds[1], argi_bounds[2], - truncate=TRUE, invert=TRUE)) + + # if an argillic is present, remove with glom truncate+invert + non.argillic <- suppressWarnings(glom(cambic, argi_bounds[1], argi_bounds[2], + truncate = TRUE, invert = TRUE)) + # commonly, a warning occurs due to argillic bottom depth at contact } else { non.argillic <- cambic } - + dark.colors <- hasDarkColors(non.argillic) non.argillic$w <- rep(1, nrow(non.argillic)) - + textures <- non.argillic[[hztexclname(p)]] - sandy.textures <- (grepl("S$", textures, ignore.case = TRUE) & + sandy.textures <- (grepl("S$", textures, ignore.case = TRUE) & !grepl("LVFS|LFS$", textures, ignore.case = TRUE)) - - if(!length(sandy.textures) | !length(dark.colors)) { + + if (!length(sandy.textures) | !length(dark.colors)) { return(empty_frame) } - + nhz <- horizons(non.argillic) + # remove horizons that are sandy or have dark colors - if(any(sandy.textures | dark.colors, na.rm = TRUE)) { - nhz <- nhz[-which(sandy.textures | dark.colors),] + if (any(sandy.textures | dark.colors, na.rm = TRUE)) { + nhz <- nhz[-which(sandy.textures | dark.colors),] } - - final <- data.frame(cambic_top=NA, cambic_bottom=NA) + + final <- data.frame(cambic_top = NA, cambic_bottom = NA) + # iterate through combinations of horizons, check topology and thickness - # finds multiple occurences of cambic horizons, excluding argillics - for(j in 1:nrow(nhz)) { - for(i in j:nrow(nhz)) { + # finds multiple occurrences of cambic horizons, excluding argillics + for (j in 1:nrow(nhz)) { + for (i in j:nrow(nhz)) { + ftop <- nhz[j:i, depths[1]] fbot <- nhz[j:i, depths[2]] - if(any(hzDepthTests(ftop, fbot))) { + + if (any(hzDepthTests(ftop, fbot))) { i <- i - 1 break; } } - pcamb.thickness <- ftop - fbot - if(length(pcamb.thickness) > 0 & - sum(pcamb.thickness, na.rm = TRUE) >= 15) { - final <- rbind(final, data.frame(cambic_top = min(ftop, na.rm=T), - cambic_bottom = max(fbot, na.rm=T))) + + pcamb.thickness <- fbot - ftop + if (length(pcamb.thickness) > 0 & sum(pcamb.thickness, na.rm = TRUE) >= 15) { + + final <- rbind(final, data.frame(cambic_top = min(ftop, na.rm = TRUE), + cambic_bottom = max(fbot, na.rm = TRUE))) } } + # construct data.frame result final <- final[complete.cases(final),] - if(nrow(final) == 0) { + if (nrow(final) == 0) { return(empty_frame) } - iddf <- data.frame(id=profile_id(p), - cambic_index=1:nrow(final)) + + iddf <- data.frame(id = profile_id(p), cambic_index = 1:nrow(final)) colnames(iddf) <- c(idname(p), "cambic_id") return(cbind(iddf, final)) } diff --git a/R/mollicEpipedon.R b/R/mollicEpipedon.R index 10c4259d1..dd3e774a3 100644 --- a/R/mollicEpipedon.R +++ b/R/mollicEpipedon.R @@ -1,14 +1,14 @@ #' Calculate the minimum thickness requirement for Mollic epipedon -#' +#' #' @description Utilize horizon depths, designations and textures in a profile to estimate the thickness requirement for the Mollic or Umbric epipedon, per criterion 6 in the U.S. Keys to Soil Taxonomy (12th Edition). -#' +#' #' @param p A single-profile SoilProfileCollection. #' @param texcl.attr Column in horizon table containing texture classes. Default: \code{guessHzTexClName(p)} #' @param clay.attr Column in horizon table containing clay contents. Default: \code{guessHzAttrName(p, 'clay', c('total','_r'))} #' @param truncate Should sliding scale (Criterion 6C) results be truncated to 18 to 25cm interval? (Experimental; Default: TRUE) #' #' @return A unit length numeric vector containing Mollic or Umbric epipedon minimum thickness requirement. -#' +#' #' @author Andrew G. Brown #' @export mollic.thickness.requirement #' @@ -16,48 +16,50 @@ #' # construct a fake profile #' spc <- data.frame(id=1, taxsubgrp = "Lithic Haploxeralfs", #' hzname = c("A","AB","Bt","BCt","R"), -#' hzdept = c(0, 20, 32, 42, 49), -#' hzdepb = c(20, 32, 42, 49, 200), +#' hzdept = c(0, 20, 32, 42, 49), +#' hzdepb = c(20, 32, 42, 49, 200), #' prop = c(18, 22, 28, 24, NA), #' texcl = c("l","l","cl", "l","br"), #' d_value = c(5, 5, 5, 6, NA), #' m_value = c(2.5, 3, 3, 4, NA), #' m_chroma = c(2, 3, 4, 4, NA)) -#' +#' #' # promote to SoilProfileCollection #' depths(spc) <- id ~ hzdept + hzdepb #' hzdesgnname(spc) <- 'hzname' #' hztexclname(spc) <- 'texcl' -#' +#' #' # print results in table -#' data.frame(id = spc[[idname(spc)]], -#' thickness_req = mollic.thickness.requirement(spc, clay.attr='prop'), -#' thickness_req_nobound = mollic.thickness.requirement(spc, +#' data.frame(id = spc[[idname(spc)]], +#' thickness_req = mollic.thickness.requirement(spc, clay.attr='prop'), +#' thickness_req_nobound = mollic.thickness.requirement(spc, #' clay.attr='prop', truncate=FALSE)) #' -mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), clay.attr = guessHzAttrName(p, 'clay', c('total','_r')), truncate = TRUE) { - +mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), + clay.attr = guessHzAttrName(p, 'clay', c('total','_r')), + truncate = TRUE) { + if(length(p) > 1) { stop("`p` must be a single-profile SoilProfileCollection") } - - # determine boundaries + + # determine boundaries # For purposes of identification of minimum thickness of mollic for field descriptions # technically it is not applying the true taxonomic rules b/c it is based on hz desgn mss <- getMineralSoilSurfaceDepth(p) - - soil_depth <- minDepthOf(p, "Cr|R|Cd|m", - no.contact.depth = 200, + + soil_depth <- minDepthOf(p, "Cr|R|Cd|m", + no.contact.depth = 200, no.contact.assigned = NA) - + if(!is.na(soil_depth) & soil_depth == 0) { return(10) } - + # if criteria aren't met in at least some part of upper 25 of mineral soil material, they won't be met deeper # it is possible there are criteria greater than 25cm that affect total mollic thickness, but here we # are only calculating the thickness _requirement_ - + # get horizon data within mineral soil surface "critical zone" cztop <- mss czbot <- mss + min(soil_depth - mss, 25, na.rm = TRUE) @@ -66,34 +68,34 @@ mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), cl warning("no mineral soil material present in profile") return(NA) } - epi <- glom(p, cztop, czbot, truncate=TRUE) - if(!inherits(epi, 'SoilProfileCollection')) { + suppressWarnings(epi <- glom(p, cztop, czbot, truncate = TRUE)) + if (!inherits(epi, 'SoilProfileCollection')) { return(NA) } - # get horizon data from mineral soil surface to bedrock, physical root restriction, + # get horizon data from mineral soil surface to bedrock, physical root restriction, # or pedogenic cementation >90% OR bottom of profile OR 250cm (~beyond SCS) soil.bottom <- min(soil_depth, deepest.bot.depth, 250, na.rm = TRUE) - sol <- glom(p, mss, soil.bottom, truncate=TRUE) + suppressWarnings(sol <- glom(p, mss, soil.bottom, truncate = TRUE)) if(!inherits(sol, 'SoilProfileCollection')) { return(NA) } - + # 6C1 - TODO: create functions for testing basic field criteria for these diagnostics # and lab data extension to support carbonates/calcic - - # Again, for purposes of identification of minimum thickness of mollic this is fast and + + # Again, for purposes of identification of minimum thickness of mollic this is fast and # probably fine 95% of the time # but technically it is not applying the true taxonomic rules b/c it is based on hz desgn cemented_depth <- depthOf(p, "m", no.contact.depth = 0, no.contact.assigned = NA) carbonate_depth <- depthOf(p, "k", no.contact.depth = 0, no.contact.assigned = NA) fragipan_depth <- depthOf(p, "x", no.contact.depth = 0, no.contact.assigned = NA) - - # calculate "shallowest of secondary carbonates/calcic, petrocalcic, duripan, fragipan" - crit6c1 <- suppressWarnings(min(carbonate_depth, - fragipan_depth, + + # calculate "shallowest of secondary carbonates/calcic, petrocalcic, duripan, fragipan" + crit6c1 <- suppressWarnings(min(carbonate_depth, + fragipan_depth, cemented_depth, na.rm=TRUE)) - - # 6C2 - identify argillic boundaries via aqp::getArgillicBounds + + # 6C2 - identify argillic boundaries via aqp::getArgillicBounds # TODO: create functions for testing basic field criteria for these diagnostics (color structure) # and lab data extension for spodic materials; can use logic from "sandy cambic" demo -- but more is needed argi_bounds <- getArgillicBounds(p, clay.attr = clay.attr, texcl.attr = texcl.attr) @@ -101,53 +103,53 @@ mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), cl if(is.finite(argi_bounds[2])) { argillic_bottom <- argi_bounds[2] } - + # AGAIN, for purposes of identification of minimum thickness of mollic this is fast and probably fine # but technically it is not applying the true taxonomic rules b/c it is based on hz desgn - natric_bottom <- maxDepthOf(p, pattern="n", top=FALSE, + natric_bottom <- maxDepthOf(p, pattern="n", top=FALSE, no.contact.assigned = NA) - oxic_bottom <- maxDepthOf(p, pattern="o", top=FALSE, + oxic_bottom <- maxDepthOf(p, pattern="o", top=FALSE, no.contact.assigned = NA) - spodic_bottom <- maxDepthOf(p, pattern="h|s", top=FALSE, + spodic_bottom <- maxDepthOf(p, pattern="h|s", top=FALSE, no.contact.assigned = NA) - - # the B|w == cambic was particularly egregious + + # the B|w == cambic was particularly egregious cambic_bottom <- suppressWarnings(max(getCambicBounds(p, argi_bounds=argi_bounds)$cambic_bottom, na.rm=TRUE)) - + # calculate "deepest of lower boundary of argillic/natric, cambic, oxic, spodic" - crit6c2 <- suppressWarnings(max(c(argillic_bottom, - natric_bottom, - cambic_bottom, - oxic_bottom, + crit6c2 <- suppressWarnings(max(c(argillic_bottom, + natric_bottom, + cambic_bottom, + oxic_bottom, spodic_bottom), na.rm=TRUE)) - + # SHORT CIRCUITS - sandy.textures <- (grepl("S$", epi[[texcl.attr]], ignore.case = TRUE) & + sandy.textures <- (grepl("S$", epi[[texcl.attr]], ignore.case = TRUE) & !grepl("LVFS|LFS$", epi[[texcl.attr]], ignore.case = TRUE)) - + if(all(sandy.textures)) { # 6A - assumes you must check for sandy textures throughout 0-25 to trigger minimum thickness of 25cm - # there is an implicit assumption that after mixing any non-sandy texture into sandy texture - # you would have non-sandy. this logic could be altered to be more "restrictive" by changing - # all() to any() -- forcing the 25cm requirement. + # there is an implicit assumption that after mixing any non-sandy texture into sandy texture + # you would have non-sandy. this logic could be altered to be more "restrictive" by changing + # all() to any() -- forcing the 25cm requirement. # - # in that case, _any_ sandy texture would imply sandy textures, importantly: precluding + # in that case, _any_ sandy texture would imply sandy textures, importantly: precluding # the 10cm requirement. - return(25) + return(25) } else { maxdepth <- suppressWarnings(max(sol$hzdepb, na.rm=TRUE)) if(is.finite(maxdepth) & maxdepth < 25) { # 6B - if all horizons above a contact are non-sandy and meet all mollic characteristics then # the minimum thickness could be only 10cm, we have filtered out sandy textures - # - # technically, the 10cm requirement requires knowledge of whether other mollic requirements are met, + # + # technically, the 10cm requirement requires knowledge of whether other mollic requirements are met, # so gives the epipedon a somewhat circular definition. This case applies in deep soils with mollic materials - # all the way to contact -- but only practically matters where a contact is less than 25cm depth, + # all the way to contact -- but only practically matters where a contact is less than 25cm depth, # so that is when we return it. return(10) } } - + # If no diagnostics are present... if(!is.finite(crit6c1) & !is.finite(crit6c2)) { # TODO: "fluventic" soils with no other diagnostics have minimum thickness of 25cm @@ -157,34 +159,34 @@ mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), cl # tolerance <- 0.1 # set a threshold for "different" carbon contents of 0.1% # # carbon-depth "fluctuations" less or equal to tolerance will be ignored # # for different data sources this number may need to be higher - + # irregular.decrease <- diff(epi$estimated_organic_carbon) < 0 # if(length(irregular.decrease[irregular.decrease >= tolerance]) { # return(25) - # } - # TODO: most pedons don't have OC measured, develop color-carbon-depth-spatial surrogate model? + # } + # TODO: most pedons don't have OC measured, develop color-carbon-depth-spatial surrogate model? return(25) } # this is criterion 6d (if "none of above" apply) return(18) } - - # calculate the most restrictive requirement from 6a, 6b, 6c + + # calculate the most restrictive requirement from 6a, 6b, 6c # which contain several restatements of the fundamental criteria for a sliding scale thickness sixcdepths <- c(crit6c1 / 3, crit6c2 / 3) - + # only some diagnostics are present per subcriteria 1 and 2 no.diag <- which(is.infinite(sixcdepths)) if(length(no.diag)) { sixcdepths <- sixcdepths[-no.diag] } - + # debug: see "true" (unrestricted 18-25 depth) # print(max(sixcdepths)) - + # the thickness requirement is based on the most restrictive condition sixcdepths.t <- sixcdepths - + # sliding scale depths based on diagnostics are truncated to [18, 25] # for investigations evaluating particular criteria, may be useful to ignore this limit if(truncate) { @@ -192,23 +194,23 @@ mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), cl sixcdepths.t[sixcdepths < 18] <- 18 } most.restrictive <- max(sixcdepths.t) - - # this should not happen with properly populated pedons + + # this should not happen with properly populated pedons # (missing horizon designations or bottom depths?) if(is.infinite(most.restrictive)) { warning(paste0("cannot evaluate mollic miniumum thickness requirement (", idname(p),":",profile_id(p),")")) } - + return(most.restrictive) } #' Find horizons with colors darker than a Munsell hue, value, chroma threshold -#' -#' @description \code{hasDarkColors} returns a boolean value by horizon representing whether darkness thresholds are met. The code is fully vectorized and deals with missing data and optional thresholds. -#' +#' +#' @description \code{hasDarkColors} returns a boolean value by horizon representing whether darkness thresholds are met. The code is fully vectorized and deals with missing data and optional thresholds. +#' #' Default arguments are set up for "5-3-3 colors" -- the basic criteria for Mollic/Umbric epipedon/mineral soil darkness. Any of the thresholds or column names can be altered. Any thresholds that are set equal to \code{NA} will be ignored. -#' +#' #' @param p A SoilProfileCollection. #' @param d_hue Optional: character vector of dry hues to match (default: NA) #' @param m_hue Optional: character vector of moist hues to match (default: NA) @@ -224,60 +226,60 @@ mollic.thickness.requirement <- function(p, texcl.attr = guessHzTexClName(p), cl #' @param mchrnm Column name containing moist chroma. #' #' @return Boolean value (for each horizon in \code{p}) reflecting whether "darkness" criteria are met. -#' +#' #' @author Andrew G. Brown -#' +#' #' @export hasDarkColors #' #' @examples #' # construct a fake profile #' spc <- data.frame(id=1, taxsubgrp = "Lithic Haploxeralfs", #' hzdesgn = c("A","AB","Bt","BCt","R"), -#' hzdept = c(0, 20, 32, 42, 49), -#' hzdepb = c(20, 32, 42, 49, 200), +#' hzdept = c(0, 20, 32, 42, 49), +#' hzdepb = c(20, 32, 42, 49, 200), #' d_value = c(5, 5, 5, 6, NA), #' m_value = c(2.5, 3, 3, 4, NA), #' m_chroma = c(2, 3, 4, 4, NA)) -#' +#' #' # promote to SoilProfileCollection #' depths(spc) <- id ~ hzdept + hzdepb -#' +#' #' # print results in table -#' data.frame(id = spc[[idname(spc)]], -#' hz_desgn = spc$hzdesgn, +#' data.frame(id = spc[[idname(spc)]], +#' hz_desgn = spc$hzdesgn, #' has_dark_colors = hasDarkColors(spc)) #' hasDarkColors <- function(p, d_hue=NA, m_hue=NA, d_value=5, d_chroma=NA, m_value=3, m_chroma=3, - dhuenm='d_hue', dvalnm = "d_value", dchrnm="d_chroma", + dhuenm='d_hue', dvalnm = "d_value", dchrnm="d_chroma", mhuenm='m_hue', mvalnm = "m_value", mchrnm = "m_chroma") { hz <- horizons(p) r <- matrix(nrow = nrow(hz), ncol = 6) - + r1 <- hz[[dhuenm]] %in% d_hue r2 <- hz[[mhuenm]] %in% m_hue r3 <- hz[[dvalnm]] <= d_value r4 <- hz[[dchrnm]] <= d_chroma r5 <- hz[[mvalnm]] <= m_value r6 <- hz[[mchrnm]] <= m_chroma - + if(length(r1) > 0) r[,1] <- r1 if(length(r2) > 0) r[,2] <- r2 if(length(r3) > 0) r[,3] <- r3 if(length(r4) > 0) r[,4] <- r4 if(length(r5) > 0) r[,5] <- r5 if(length(r6) > 0) r[,6] <- r6 - + required <- !is.na(c(d_hue, m_hue, d_value, d_chroma, m_value, m_chroma)) risna <- apply(r, 2, function(x) all(is.na(x))) - + # if all data are missing, return NA for each horizon if(all(required == risna)) { return(rep(NA, nrow(p))) } - + # return only required results r <- r[,required] - + # result may contain NA for individual horizons without color data if(is.null(dim(r))) { # single horizon result diff --git a/R/mutate.R b/R/mutate.R index 77ff082ed..d11e3b99b 100644 --- a/R/mutate.R +++ b/R/mutate.R @@ -6,27 +6,27 @@ #' @param ... Comma-separated set of R expressions e.g. \code{thickness = hzdepb - hzdept, hzdepm = hzdept + round(thk / 2)} #' @return A SoilProfileCollection. #' @author Andrew G. Brown. -#' +#' #' @rdname mutate #' @export mutate if (!isGeneric("mutate")) setGeneric("mutate", function(object, ...) standardGeneric("mutate")) setMethod("mutate", signature(object = "SoilProfileCollection"), function(object, ...) { - if(requireNamespace("rlang", quietly = TRUE)) { - + if (requireNamespace("rlang", quietly = TRUE)) { + # capture expression(s) at function x <- rlang::enquos(..., .named = TRUE) - + # create composite object to facilitate eval_tidy data <- compositeSPC(object) - + for(n in names(x)) { foo <- rlang::eval_tidy(x[[n]], data) object[[n]] <- foo data[[n]] <- foo } - + return(object) } else { stop("package 'rlang' is required for mutate", .call=FALSE) diff --git a/R/permute_profile.R b/R/permute_profile.R index 6536821a6..c18e80b30 100644 --- a/R/permute_profile.R +++ b/R/permute_profile.R @@ -5,15 +5,15 @@ #' @param boundary.attr Horizon attribute containing numeric "standard deviations" reflecting boundary transition distinctness #' @param min.thickness Minimum thickness of permuted horizons (default: 1) #' @param soildepth Depth below which horizon depths are not permuted (default: NULL) -#' +#' #' @description This method is most "believable" when used to _gently_ permute the data, on the order of moving boundaries a few centimeters in either direction. The nice thing about it is it can leverage semi-quantitative (ordered factor) levels of boundary distinctness/topography for the upper and lower boundary of individual horizons, given a set of assumptions to convert classes to a "standard deviation" (see example). -#' -#' If you imagine a normal curve with its mean centered on the vertical (depth axis) at a RV horizon depth. By the Empirical Rule for Normal distribution, two "standard deviations" above or below that RV depth represent 95% of the "volume" of the boundary. -#' -#' So, a standard deviation of 1-2cm would yield a "boundary thickness" in the 3-5cm range ("clear" distinctness class). -#' -#' Of course, boundaries are not symmetrical and this is at best an approximation for properties like organic matter, nutrients or salts that can have strong depth-dependence within horizons. Also, boundary topography is non-uniform. There are definitely ways to implement other distributions, but invokes more detailed assumptions about field data that are generally only semi-quantiative or are not available. -#' +#' +#' If you imagine a normal curve with its mean centered on the vertical (depth axis) at a RV horizon depth. By the Empirical Rule for Normal distribution, two "standard deviations" above or below that RV depth represent 95% of the "volume" of the boundary. +#' +#' So, a standard deviation of 1-2cm would yield a "boundary thickness" in the 3-5cm range ("clear" distinctness class). +#' +#' Of course, boundaries are not symmetrical and this is at best an approximation for properties like organic matter, nutrients or salts that can have strong depth-dependence within horizons. Also, boundary topography is non-uniform. There are definitely ways to implement other distributions, but invokes more detailed assumptions about field data that are generally only semi-quantiative or are not available. +#' #' Future implementations may use boundary topography as a second hierarchical level (e.g. trig-based random functions), but think that distinctness captures the "uncertainty" about horizon separation at a specific "point" on the ground (or line in the profile quite well, and the extra variation may be hard to interpret, in general. #' #' @return A SoilProfileCollection with n permutations of p. @@ -24,28 +24,28 @@ #' # # example with sp1 (using boundary distinctness) #' data("sp1") #' depths(sp1) <- id ~ top + bottom -#' +#' #' # specify "standard deviation" for boundary thickness #' # consider a normal curve centered at boundary RV depth #' # lookup table: ~maximum thickness of boundary distinctness classes, divided by 3 #' bound.lut <- c('V'=0.5,'A'=2,'C'=5,'G'=15,'D'=45) / 3 #' -#' ## V A C G D -#' ## 0.1666667 0.6666667 1.6666667 5.0000000 15.0000000 +#' ## V A C G D +#' ## 0.1666667 0.6666667 1.6666667 5.0000000 15.0000000 #' #' sp1$bound_sd <- bound.lut[sp1$bound_distinct] -#' +#' #' # hold any NA boundary distinctness constant #' sp1$bound_sd[is.na(sp1$bound_sd)] <- 0 -#' +#' #' quantile(sp1$bound_sd, na.rm = TRUE) #' p <- sp1[3] # # example with loafercreek (no boundaries) # library(soilDB) # data("loafercreek") -# # -# # # assume boundary sd is 1/12 midpoint of horizon depth +# # +# # # assume boundary sd is 1/12 midpoint of horizon depth # # (i.e. generally increases/less well known with depth) # # # loafercreek <- mutate(loafercreek, midpt = (hzdepb - hzdept) / 2 + hzdept, @@ -53,14 +53,14 @@ # quantile(loafercreek$bound_sd) # p <- loafercreek[1] -permute_profile <- function(p, n = 100, boundary.attr, +permute_profile <- function(p, n = 100, boundary.attr, min.thickness = 1, soildepth = NULL) { hz <- horizons(p) bounds <- hz[[boundary.attr]] depthz <- horizonDepths(p) mindepth <- min(hz[[depthz[1]]]) - + if(!is.numeric(bounds) | !length(bounds) == nrow(p)) { stop("`boundary_attr` must be refer to a numeric horizon attribute in `p` representing standard deviation of horizon boundary thickness") } @@ -72,114 +72,114 @@ permute_profile <- function(p, n = 100, boundary.attr, if(!checkHzDepthLogic(p)$valid) { stop("one or more horizon depth logic tests failed for object `p`") } - + # re-write of aqp::sim() for boundaries. permute bottom depths instead of thickness # it is hard to conceive of horizon boundaries in an individual pedon in terms of # standard deviations of total horizon thickness... though they are clearly related bottomdepths <- hz[[depthz[2]]] - + # do not vary layers below `soildepth` (if not NULL) can be arbitrary depth if(!is.null(soildepth)) { if(!is.na(soildepth) & soildepth >= mindepth) { - bounds[bottomdepths > soildepth] <- 0 + bounds[bottomdepths > soildepth] <- 0 } - } - + } + # for each horizon bottom depth (boundary) calculate a gaussian offset # from the representative value recorded in the pedon descripton res <- do.call('rbind', lapply(1:nrow(p), function(i) { new <- rnorm(n, bottomdepths[i], bounds[i]) - + # this is a bit non-kosher, but rather than sorting to fix random depths # that may be out of order, replace them iteratively until there are none # it is possible to specify SDs so large the loop below will not converge, # so a break is triggered at 1000 iterations. - + # in practice, qc warnings for improbably large SD would be useful - # say, if the SD is greater than 1/3 the hz thickness, you are likely to - # generate extreme values prone to causing logic errors; + # say, if the SD is greater than 1/3 the hz thickness, you are likely to + # generate extreme values prone to causing logic errors; # could be data entry error or improbable class assignment # # with irregular bounds, high SD could be intentional/desired -- if unstable - # - enforcing some sort of sorting? + # - enforcing some sort of sorting? # - additional random processes: waves for wavy/irregular - # - allowing irregular boundaries to eclipse/omit thin layers? - # - presence/absence of broken horizons; could this be a separate random process? would it require that volume or other % area field populated? - + # - allowing irregular boundaries to eclipse/omit thin layers? + # - presence/absence of broken horizons; could this be a separate random process? would it require that volume or other % area field populated? + idx <- 1 counter <- 0 # TODO: do better while(length(idx) > 0) { if(counter > 1000) { break - } - idx <- which(new <= bottomdepths[pmax(1, i - 1)] | + } + idx <- which(new <= bottomdepths[pmax(1, i - 1)] | new >= bottomdepths[pmin(i + 1, length(bottomdepths))]) new[idx] <- rnorm(length(idx), bottomdepths[i], bounds[i]) counter <- counter + 1 } - + # finally, no depths should be negative return(pmax(new, 0)) })) - + # aqp only supports integer depths res <- round(res) - + # find layers less than min thickness t1 <- apply(res, 2, function(x) diff(c(mindepth, x))) idx <- t1 < min.thickness res[idx] <- res[idx] + (min.thickness - t1[idx] + 1e-5) res <- round(res) - + # allocate a list for n-profile result pID <- 1:n profiles <- vector('list', n) - + profiles <- lapply(pID, function(i) { p.sub <- hz - + # create new idname and hzidname - p.sub$pID <- i - p.sub$hzID <- p.sub$hzID * i - - # insert new depths + p.sub$pID <- as.character(i) + p.sub$hzID <- as.character(1:length(p.sub$hzID) * i) + + # insert new depths nd <- (res[,i]) #TODO: sort is rarely needed, ensures topological # but not statistical correctness? p.sub[[depthz[1]]] <- c(mindepth, nd)[1:nrow(p)] p.sub[[depthz[2]]] <- nd - + test <- hzDepthTests(p.sub[[depthz[1]]], p.sub[[depthz[2]]]) if(any(test)) { stop(paste("one or more horizon logic tests failed for realization:", i)) } - + return(p.sub) }) - + # fast "union" with no checks since we know the origin o.h <- do.call('rbind', profiles) o.s <- data.frame(site(p), pID = pID, row.names = NULL) d <- diagnostic_hz(p) o.d <- data.frame() - if(length(d) != 0) { + if (length(d) != 0) { o.d <- do.call('rbind', lapply(pID, function(i) { data.frame(pID = i, d, row.names = NULL) })) } re <- restrictions(p) o.r <- data.frame() - if(length(re) > 0) { + if (length(re) > 0) { o.r <- do.call('rbind', lapply(pID, function(i) { data.frame(pID = i, re, row.names = NULL) })) } - + # always drop spatial data -- still present in site o.sp <- new('SpatialPoints') - + metadat <- metadata(p) - + # TODO: alter metadata to reflect the processing done here? res <- SoilProfileCollection(idcol='pID', depthcols=horizonDepths(p), @@ -194,16 +194,16 @@ permute_profile <- function(p, n = 100, boundary.attr, ## compare permute_profile and sim, using same estimated SD # calculate permuations of bottom depths using boundary deviations -#system.time(res <- permute_profile(p, n=1000, boundary_attr = "bound_sd", +#system.time(res <- permute_profile(p, n=1000, boundary_attr = "bound_sd", # min.thickness = 1)) -# +# # # calculate permuations of horizon thickness using horizon thickness deviation # system.time(res2 <- sim(p, n=1000, hz.sd = p$bound_sd, min.thick = 1)) -# +# # # superficially similar output # plot(res[1:10,]) # plot(res2[1:10,]) -# +# # # compare slab'd result # s.res <- slab(res, ~ prop, slab.structure = 1) # s.res2 <- slab(res2, ~ prop, slab.structure = 1) @@ -215,11 +215,11 @@ permute_profile <- function(p, n = 100, boundary.attr, # plot(x=s.res$p.q50, y=s.res$top, ylim=c(100,0), type="l") # lines(x=s.res$p.q5, y=s.res$top, ylim=c(100,0), col="blue", lty=2) # lines(x=s.res$p.q95, y=s.res$top, ylim=c(100,0), col="blue", lty=2) -# +# # lines(x=s.res2$p.q50, y=s.res2$top, ylim=c(100,0), type="l", lwd=2) # lines(x=s.res2$p.q5, y=s.res2$top, ylim=c(100,0), col="green", lty=2) # lines(x=s.res2$p.q95, y=s.res2$top, ylim=c(100,0), col="green", lty=2) -# +# # hzdesgnname(res) <- "name" # hztexclname(res) <- "texture" # res$clay <- res$prop diff --git a/tests/testthat/test-DT-tbl.R b/tests/testthat/test-DT-tbl.R index 13dc5b2bd..d8e90cb31 100644 --- a/tests/testthat/test-DT-tbl.R +++ b/tests/testthat/test-DT-tbl.R @@ -101,10 +101,38 @@ res <- lapply(dfclasses, function(use_class) { expect_message(depths(test) <- id ~ top + bottom, c("converting profile IDs from integer to character")) + # add fake coordinates + crds <- data.frame(id = profile_id(test), + y = rnorm(length(test)), + x = rnorm(length(test))) + site(test) <- crds + + # promote to spatial + coordinates(test) <- ~ x + y + proj4string(test) <- "+proj=longlat +datum=WGS84" + + # show method should be produce output without error + expect_output(show(test)) + expect_output(show(test[0,])) + + # fill in diagnostics and restrictions with fake data + diagnostic_hz(test) <- data.frame(id = profile_id(test), + featkind = "foo", + featdept = 0, featdepb = 10) + restrictions(test) <- data.frame(id = profile_id(test), + restrkind = "bar", + restrdept = 0, restrdepb = 10) + # try the character vector interface too expect_message(depths(test2) <- c("id", "top", "bottom"), c("converting profile IDs from integer to character")) + # test rebuild + expect_message(rebuildSPC(test2), "using `hzID` as a unique horizon ID") + + # test enforce_df_class + expect_silent(aqp:::.enforce_df_class(test2, use_class)) + # "normalize" (horizon -> site) a site-level attribute site(test) <- ~ siteprop @@ -353,8 +381,15 @@ res <- lapply(dfclasses, function(use_class) { # subApply works as expected expect_equal(length(subApply(sp1df, function(p) - TRUE)), length(sp1df))}) + TRUE)), length(sp1df)) + }) + if (use_class == "data.table") { + test_that("data.table specific", { + expect_equal(min(sp1df), 59) + expect_equal(max(sp1df), 240) + }) + } }) }) diff --git a/tests/testthat/test-SPC-objects.R b/tests/testthat/test-SPC-objects.R index 52292fa90..abbefab86 100644 --- a/tests/testthat/test-SPC-objects.R +++ b/tests/testthat/test-SPC-objects.R @@ -301,7 +301,7 @@ test_that("SPC depth columns get/set ", { test_that("SPC min/max overrides work as expected", { set.seed(20202) - df <- lapply(1:10, random_profile, SPC=TRUE) + df <- lapply(1:10, random_profile, SPC = TRUE) df <- union(df) ## visually inspect output @@ -311,6 +311,9 @@ test_that("SPC min/max overrides work as expected", { # both min and max should return 10cm expect_equal(min(df), 44) expect_equal(max(df), 134) + + expect_equal(min(df, v = "p2"), 44) + expect_equal(max(df, v = "p2"), 134) }) test_that("SPC horizonNames get/set ", { @@ -408,12 +411,12 @@ test_that("SPC horizon designation/texcl name get/set ", { # no column in horizon table 'xxx' expect_error(hzdesgnname(sp1) <- 'xxx') - # message when setting to empty (sets slot to character(0)) - # NOTE: cannot have this be so verbose, needs to happen during subsetting - expect_message(hzdesgnname(sp1) <- '') + # set to empty + expect_silent(hzdesgnname(sp1) <- '') - # error when slot is empty and using accessor - expect_error(designations <- hzDesgn(sp1)) + # null when cannot find the column name + expect_silent(designations <- hzDesgn(sp1)) + expect_true(is.null(designations)) }) test_that("SPC horizon ID get/set ", { @@ -504,6 +507,7 @@ test_that("SPC horizon ID init conflicts", { x <- as(x, 'data.frame') expect_message(depths(x) <- id ~ top + bottom, "^using") expect_equivalent(hzidname(x), 'hzID') + expect_true(checkSPC(x)) # decompose, add non-unique column conflicing with hzID x <- sp1 @@ -613,9 +617,21 @@ test_that("basic integrity checks", { # inverting the horizon order makes it invalid expect_true(!spc_in_sync(spc)$valid) + # an empty spc derived from invalid spc is valid + expect_true(spc_in_sync(spc[0,])$valid) + # reordering the horizons with reorderHorizons resolves integrity issues expect_true(spc_in_sync(reorderHorizons(spc))$valid) + # default reordering relies on intact metadata + spc@metadata$target.order <- rev(spc@metadata$target.order) + expect_false(spc_in_sync(reorderHorizons(spc))$valid) + + # removing the metadata works because target order matches sequential order + # this cannot be guaranteed to be the case in general but is a reasonable default + spc@metadata$target.order <- NULL + expect_true(spc_in_sync(reorderHorizons(spc))$valid) + # reordering horizons with any order works, even if invalid spc <- reorderHorizons(spc, target.order = c(20:40,1:19)) expect_true(!spc_in_sync(spc)$valid) diff --git a/tests/testthat/test-acc-unc.R b/tests/testthat/test-acc-unc.R new file mode 100644 index 000000000..7ae8df5c7 --- /dev/null +++ b/tests/testthat/test-acc-unc.R @@ -0,0 +1,22 @@ +context("classification accuracy and uncertainty") + +# hypothetical three class probabilitites: 0.5 0.3 0.2 +probs <- c(5,3,2) / 10 + +test_that("shannonEntropy", { + expect_equal(round(shannonEntropy(probs), 4), 1.4855) + expect_equal(round(shannonEntropy(probs, 3), 4), 0.9372) +}) + +test_that("confusionIndex", { + expect_equal(confusionIndex(probs), 0.8) +}) + +test_that("brierScore", { + dff <- data.frame(diag(3)) + expect_equal(brierScore(dff, c("X1","X2","X3")), 1) + + set.seed(1) + dff2 <- data.frame(matrix(runif(9), 3, 3)) + expect_equal(round(brierScore(dff2, c("X1","X2","X3")), 2), 1.31) +}) diff --git a/tests/testthat/test-aggregateColor.R b/tests/testthat/test-aggregateColor.R index e27890529..febf947a0 100644 --- a/tests/testthat/test-aggregateColor.R +++ b/tests/testthat/test-aggregateColor.R @@ -34,6 +34,12 @@ test_that("basic functionality", { }) ## TODO: test for expected error conditions +test_that("expected error conditions", { + expect_error(aggregateColor(x, groups='foo', col='soil_color')) + expect_error(aggregateColor(x, groups='genhz', col='foo')) + expect_error(aggregateColor(x, groups='genhz', col='soil_color', colorSpace = 'foo')) + expect_error(aggregateColor(x, groups='genhz', col='soil_color', k=NA)) +}) test_that("manual calculation using CIE2000 and LAB, single profile", { @@ -42,10 +48,13 @@ test_that("manual calculation using CIE2000 and LAB, single profile", { x$genhz <- rep('A', times=nrow(x)) a <- aggregateColor(x, groups='genhz', col='soil_color') a2 <- aggregateColor(x, groups='genhz', col='soil_color', colorSpace = 'LAB') + a3 <- aggregateColor(x, groups='genhz', col='soil_color', colorSpace = 'LAB', k=1) # known number of horizons / color # table(x$soil_color) expect_equal(a$scaled.data$A$n.hz, c(2,1,1,1)) + expect_equal(a2$scaled.data$A$n.hz, c(2,1,1,1)) + expect_equal(a3$scaled.data$A$n.hz, 5) expect_equal(round(a$scaled.data$A$weight, 3), c(0.342, 0.270, 0.258, 0.129)) diff --git a/tests/testthat/test-aqp.R b/tests/testthat/test-aqp.R new file mode 100644 index 000000000..97f0ff740 --- /dev/null +++ b/tests/testthat/test-aqp.R @@ -0,0 +1,8 @@ +context("aqp package environment") + +test_that("defaults", { + expect_equal(getOption(".aqp.show.n.cols"), 10) + options(.aqp.show.n.cols = 100) + expect_equal(getOption(".aqp.show.n.cols"), 100) + expect_silent(aqp:::.onLoad("foo","bar")) # libname and pkgname not used at present +}) diff --git a/tests/testthat/test-argillic.R b/tests/testthat/test-argillic.R index 905ae8964..62be4b2a1 100644 --- a/tests/testthat/test-argillic.R +++ b/tests/testthat/test-argillic.R @@ -5,27 +5,27 @@ depths(sp1) <- id ~ top + bottom site(sp1) <- ~ group p <- sp1[1] -clay.attr <- 'prop' # clay contents % +clay.attr <- 'prop' # clay contents % texcl.attr <- 'texture' # class containing textural class (for finding sandy textures) #implicitly testing the basic logic of this function :) -threshold.fun <- crit.clay.argillic +threshold.fun <- crit.clay.argillic #standard for argillic vertical.distance <- 30 test_that("get.increase.matrix() (used for getArgillicBounds())", { - m <- get.increase.matrix(p, clay.attr, threshold.fun, vertical.distance) - + m <- get.increase.matrix(p, clay.attr, threshold.fun, vertical.distance) + # correct data type? expect_true(inherits(m, 'matrix')) - + # is square matrix? expect_true(nrow(m) == ncol(m)) - + # no TRUE in lower triangle? expect_true(all(!(m * lower.tri(m)))) - + # correct number of eluvial horizons identified in first illuvial column? # this is also a test of the underlying crit.clay.argillic expect_equal(colSums(m)[4], 2) @@ -34,36 +34,42 @@ test_that("get.increase.matrix() (used for getArgillicBounds())", { test_that("argillic.clay.increase() (used for getArgillicBounds())", { #make sure the main wrapper method for the increase functions works d <- argillic.clay.increase.depth(p, clay.attr = 'prop') - + # this is also a test of the underlying crit.clay.argillic expect_equal(d, 49) }) test_that("getArgillicBounds()", { - + # name and texture class are guessable d <- getArgillicBounds(p, clay.attr='prop') - + # this makes sure estimateSoilDepth() isn't broken... expect_equivalent(d, c(49, 89)) - + # no error when hzdesgn and texcl.attr are unknown, due to guessing function d1 <- getArgillicBounds(p, hzdesgn='foo', clay.attr='prop', texcl.attr = 'bar') expect_equivalent(d1, c(49, 89)) - - # deliberately break the reasoning guessing + + # deliberately break the reasoning guessing # returns correct result because of slots p$goo <- p$name p$boo <- p$texture p$name <- NULL p$texture <- NULL - + expect_error(getArgillicBounds(p, hzdesgn='foo', clay.attr='prop', texcl.attr = 'bar')) - + # set the desgn name and texture class slots hzdesgnname(p) <- "goo" hztexclname(p) <- "boo" - + d2 <- getArgillicBounds(p, hzdesgn='foo', clay.attr='prop', texcl.attr = 'bar') expect_equivalent(d2, c(49, 89)) }) + +test_that("getArgillicBounds - error conditions", { + expect_error(getArgillicBounds(p, hzdesgn = "foo")) + expect_error(getArgillicBounds(p, texcl.attr = "foo")) + expect_error(getArgillicBounds(p, clay.attr = "foo")) +}) diff --git a/tests/testthat/test-cambic.R b/tests/testthat/test-cambic.R new file mode 100644 index 000000000..18f5fe55c --- /dev/null +++ b/tests/testthat/test-cambic.R @@ -0,0 +1,49 @@ +context("estimate cambic boundaries") + +# construct a fake profile +# one dark colored horizon (AB) and two cambic subhorizons Bw + BC +spc <- data.frame(id = 1, taxsubgrp = "Lithic Haploxerepts", + hzname = c("A","AB","Bw","BC","R"), + hzdept = c(0, 20, 32, 42, 49), + hzdepb = c(20, 32, 42, 49, 200), + clay = c(19, 22, 22, 21, NA), + texcl = c("l","l","l","l","br"), + d_value = c(5, 5, 5, 6, NA), + m_value = c(2.5, 3, 3, 4, NA), + m_chroma = c(2, 3, 4, 4, NA)) + +# promote to SoilProfileCollection +depths(spc) <- id ~ hzdept + hzdepb +hzdesgnname(spc) <- 'hzname' +hztexclname(spc) <- 'texcl' + +test_that("getCambicBounds - basic functionality", { + dfbound <- getCambicBounds(spc) + expect_equal(nrow(dfbound), 1) + expect_equal(as.numeric(dfbound[,c("cambic_top","cambic_bottom")]), c(32,49)) + + # exclude by entry of non-cambic bounds + expect_equal(nrow(getCambicBounds(spc, argi_bounds = c(32,49))), 0) + + # empty spc input + expect_error(getCambicBounds(spc[0,])) +}) + +test_that("getCambicBounds - special cases", { + spc2 <- spc + spc2$texcl <- "S" # all sandy textures + dfbound <- getCambicBounds(spc2) + expect_equal(nrow(dfbound), 0) + + spc2 <- spc + spc2$hzname <- c("A","Bt","BE","Bhs","Bt'") + spc2$clay <- c(11,18,16,17,22) + dfbound <- getCambicBounds(spc2) + expect_equal(nrow(dfbound), 0) +}) + +test_that("getArgillicBounds - error conditions", { + expect_error(getArgillicBounds(spc2, hzdesgn = "foo")) + expect_error(getArgillicBounds(spc2, texcl.attr = "foo")) + expect_error(getArgillicBounds(spc2, clay.attr = "foo")) +}) diff --git a/tests/testthat/test-checkHzDepthLogic.R b/tests/testthat/test-checkHzDepthLogic.R index 4126bbf4d..655022599 100644 --- a/tests/testthat/test-checkHzDepthLogic.R +++ b/tests/testthat/test-checkHzDepthLogic.R @@ -5,17 +5,29 @@ data(sp3) expect_silent({depths(sp3) <- id ~ top + bottom}) +test_that("hzDepthTests works as expected", { + hdep <- horizonDepths(sp3) + + # vector of top and bottom depths -> 4 logical test results + res <- hzDepthTests(sp3[[hdep[1]]], sp3[[hdep[2]]]) + expect_equal(names(res)[res], c("depthLogic","overlapOrGap")) # depthLogic & overlapOrGap errors + expect_equal(length(res), 4) + + # mismatched lengths (top and bottom must have same number of values) + expect_error(hzDepthTests(sp3[[hdep[1]]], sp3[[hdep[2]]][1])) +}) + test_that("checkHzDepthLogic() works as expected", { - + # these data should be clean res <- checkHzDepthLogic(sp3) - + # result is an data.frame expect_true(inherits(res, 'data.frame')) - + # number of rows should match length(SPC) expect_true(nrow(res) == length(sp3)) - + # all clear expect_true(all( ! res$depthLogic)) expect_true(all( ! res$sameDepth)) @@ -26,24 +38,24 @@ test_that("checkHzDepthLogic() works as expected", { test_that("checkHzDepthLogic() depth logic errors", { - + # local copy x <- sp3[1, ] x$top[1] <- 10 - res <- checkHzDepthLogic(x) - + res <- checkHzDepthLogic(x) + # errors only affect the first profile in this set expect_true(res$depthLogic[1]) expect_false(res$valid[1]) }) test_that("checkHzDepthLogic() same top / bottom depths", { - + # local copy x <- sp3[7, ] x$bottom[3] <- x$top[3] - res <- checkHzDepthLogic(x) - + res <- checkHzDepthLogic(x) + # errors only affect the first profile in this set expect_true(res$sameDepth[1]) expect_false(res$valid[1]) @@ -51,12 +63,12 @@ test_that("checkHzDepthLogic() same top / bottom depths", { test_that("checkHzDepthLogic() NA in depths", { - + # local copy x <- sp3[4, ] x$bottom[3] <- NA - res <- checkHzDepthLogic(x) - + res <- checkHzDepthLogic(x) + # errors only affect the first profile in this set expect_true(res$missingDepth[1]) expect_false(res$valid[1]) @@ -64,13 +76,13 @@ test_that("checkHzDepthLogic() NA in depths", { test_that("checkHzDepthLogic() gap", { - + # local copy x <- sp3[8, ] # create a gap x$top[4] <- 82 - res <- checkHzDepthLogic(x) - + res <- checkHzDepthLogic(x) + # errors only affect the first profile in this set expect_true(res$overlapOrGap[1]) expect_false(res$valid[1]) @@ -78,13 +90,13 @@ test_that("checkHzDepthLogic() gap", { test_that("checkHzDepthLogic() overlap", { - + # local copy x <- sp3[8, ] # create a gap x$top[4] <- 75 - res <- checkHzDepthLogic(x) - + res <- checkHzDepthLogic(x) + # errors only affect the first profile in this set expect_true(res$overlapOrGap[1]) expect_false(res$valid[1]) diff --git a/tests/testthat/test-colorContrast.R b/tests/testthat/test-colorContrast.R index 3e44c7576..d2085ce58 100644 --- a/tests/testthat/test-colorContrast.R +++ b/tests/testthat/test-colorContrast.R @@ -10,81 +10,84 @@ m2 <- c('5YR 3/4', '7.5YR 4/4', '2.5YR 2/2', '7.5YR 6/3') ## tests test_that("huePosition works as expected", { - + x <- c('2.5YR', '7.5YR', '10YR', '5BG') z <- huePosition(x) - + # manually counted on the Munsell wheel # https://www.nrcs.usda.gov/wps/portal/nrcs/detail/soils/ref/?cid=nrcs142p2_053569 expect_equal(z, c(4, 6, 7, 21)) - + # bogus input should result in NA expect_true(is.na(huePosition('10YR 3/3'))) - + }) test_that("contrastClass works as expected", { - + ## hand-done tests - + # 10YR 6/3 vs 5YR 3/4 x <- contrastClass(v1=6, c1=3, v2=3, c2=4, dH=2, dV=3, dC=1, verbose = TRUE) expect_true(x$faint$res == 'Prominent') expect_equivalent(unlist(x$faint[, c('f.case1', 'f.case2', 'f.case3', 'low.value.chroma')]), c(FALSE, FALSE, FALSE, FALSE)) expect_equivalent(unlist(x$distinct[, c('d.case1', 'd.case2', 'd.case3')]), c(FALSE, FALSE, FALSE)) - + # 7.5YR 3/3 vs 7.5YR 4/4 x <- contrastClass(v1=3, c1=3, v2=4, c2=4, dH=0, dV=1, dC=1, verbose = TRUE) expect_true(x$faint$res == 'Faint') expect_equivalent(unlist(x$faint[, c('f.case1', 'f.case2', 'f.case3', 'low.value.chroma')]), c(TRUE, FALSE, FALSE, FALSE)) expect_equivalent(unlist(x$distinct[, c('d.case1', 'd.case2', 'd.case3')]), c(FALSE, FALSE, FALSE)) - + # 10YR 2/2 vs 2.5YR 2/2 x <- contrastClass(v1=2, c1=2, v2=2, c2=2, dH=0, dV=0, dC=0, verbose = TRUE) expect_true(x$faint$res == 'Faint') expect_equivalent(unlist(x$faint[, c('f.case1', 'f.case2', 'f.case3', 'low.value.chroma')]), c(TRUE, FALSE, FALSE, TRUE)) expect_equivalent(unlist(x$distinct[, c('d.case1', 'd.case2', 'd.case3')]), c(FALSE, FALSE, FALSE)) - + # 7.5YR 3/4 vs 7.5YR 6/3 x <- contrastClass(v1=3, c1=4, v2=5, c2=3, dH=0, dV=3, dC=1, verbose = TRUE) expect_true(x$faint$res == 'Distinct') expect_equivalent(unlist(x$faint[, c('f.case1', 'f.case2', 'f.case3', 'low.value.chroma')]), c(FALSE, FALSE, FALSE, FALSE)) expect_equivalent(unlist(x$distinct[, c('d.case1', 'd.case2', 'd.case3')]), c(TRUE, FALSE, FALSE)) - + + # Error: inputs must all have the same length + expect_error(x <- contrastClass(v1=3, c1=4, v2=5, c2=3, dH=0, dV=3, dC=numeric(0), verbose = TRUE)) + ## TODO: test entire range of rules/cases - + }) test_that("colorContrast works as expected", { - + # contrast metrics d <- colorContrast(m1, m2) - + # output should be a data.frame expect_true(inherits(d, 'data.frame')) expect_true(nrow(d) == length(m1)) - + # first two columns should contain original colors expect_true(all(d$m1 == m1)) expect_true(all(d$m2 == m2)) - + # color contrast should be an ordered factor expect_true(is.factor(d$cc)) expect_true(is.ordered(d$cc)) - + }) test_that("colorContrast fails as expected", { - + # m1/m2 not same length ---> error expect_error(colorContrast(m1[1], m2)) - + # bogus hues -> dH and dE00 are NA d <- colorContrast('10FG 2/3', '4ZZ 4/5') expect_true(is.na(d$dH)) expect_true(is.na(d$dE00)) - + # bogus Munsell colors, all NA d <- colorContrast('123sdf', '345gg') expect_true(all(is.na(d[, -c(1:2)]))) @@ -92,17 +95,17 @@ test_that("colorContrast fails as expected", { test_that("valid results", { - + # contrast metrics d <- colorContrast(m1, m2) - + # hand-checked expect_equal(d$dH, c(2, 0, 3, 0)) expect_equal(d$dV, c(3, 1, 0, 3)) expect_equal(d$dC, c(1, 1, 0, 1)) expect_equal(as.character(d$cc), c('Prominent', 'Faint', 'Faint', 'Distinct')) - + ## TODO add some less-common colors - + }) diff --git a/tests/testthat/test-denormalize.R b/tests/testthat/test-denormalize.R index 224d157da..910c5208d 100644 --- a/tests/testthat/test-denormalize.R +++ b/tests/testthat/test-denormalize.R @@ -13,9 +13,11 @@ test_that("denormalize result is 1:1 with horizons", { # name the attribute something different (e.g. `hz.sitevar`) to prevent collision with the site attribute # the attributes can have the same name but you will then need site() or horizons() to access explicitly sp1.hz.sitevar <- denormalize(sp1, 'sitevar') - + + expect_error(sp1.hz.sitevar <- denormalize(sp1, 'foo')) + # compare number of horizons to number of values in denormalize result expect_equal(nrow(sp1), length(sp1.hz.sitevar)) # check that the output is 1:1 with horizon - + sp1$hz.sitevar <- sp1.hz.sitevar }) diff --git a/tests/testthat/test-dplyr-verbs.R b/tests/testthat/test-dplyr-verbs.R new file mode 100644 index 000000000..1a6f6f7fd --- /dev/null +++ b/tests/testthat/test-dplyr-verbs.R @@ -0,0 +1,36 @@ +context("dplyr-like verbs") + +data(sp3) +depths(sp3) <- id ~ top + bottom +site(sp3)$group <- "A" +sp3$group[3:6] <- "B" + +test_that("mutate & mutate_profile", { + + # mutate + res <- mutate(sp3, thickness = bottom - top) + expect_equal(mean(res$thickness), 18.5652174) + # plot(res, color="thickness") + + # mutate_profile + res <- mutate_profile(res, relthickness = (bottom - top) / sum(thickness)) + expect_equal(mean(res$relthickness), 0.2173913) + # plot(res, color="relthickness") +}) + +test_that("group_by & summarize", { + + sp3 <- group_by(sp3, group) + expect_equal(metadata(sp3)$aqp_group_by, "group") + + # mean for A and B group horizon data + summa <- summarize(sp3, round(mean(clay))) + expect_equal(summa, structure(list(group = c("A", "B"), + `round(mean(clay))` = c(11, 29)), + class = "data.frame", row.names = c(NA, -2L))) +}) + +# +# test_that("", { +# +# }) diff --git a/tests/testthat/test-genhz.R b/tests/testthat/test-genhz.R index a4b96c9f2..445de2f7e 100644 --- a/tests/testthat/test-genhz.R +++ b/tests/testthat/test-genhz.R @@ -6,37 +6,37 @@ x <- c('A', 'AC', 'Bt1', '^AC', 'C', 'BC', 'CB') ## tests test_that("basic pattern matching", { - + # the third pattern will steal from the second n <- c('A', '^AC', 'C') p <- c('A', '\\^A', 'C') res <- generalize.hz(x, new = n, pat=p, non.matching.code = 'not-used') - + # matching only text, not factor levels expect_equal(as.character(res), c('A', 'C', 'not-used', 'C', 'C', 'C', 'C')) - + # check levels: these should match the ording of `n` + non matching code expect_equal(levels(res), c('A', '^AC', 'C', 'not-used')) }) test_that("advanced pattern matching, requires perl", { - + # the third pattern may steal from the second n <- c('A', '^AC', 'C') # A -- ^A -- C without preceding A p <- c('A', '\\^A', '(? 16] <- "Bt" + +test_that("guessGenHzLevels works as expected", { + expect_equal(as.numeric(guessGenHzLevels(sp3)$median.depths), c(5,40,44)) +}) + +test_that("hzTransitionProbabilities works as expected", { + res <- hzTransitionProbabilities(sp3, "genhz") + expect_equal(res, structure(c(0, 0, 0, 0.375, 1, 0.111111111111111, + 0.625, 0, 0.888888888888889), .Dim = c(3L, 3L), + .Dimnames = list(c("A", "Bt", "C"), + c("A", "Bt", "C")), ties = FALSE)) + res <- hzTransitionProbabilities(sp3, "genhz", loopTerminalStates = TRUE) + expect_equal(res, structure(c(0, 0, 0, 0.375, 1, 0.111111111111111, + 0.625, 0, 0.888888888888889), + .Dim = c(3L, 3L), .Dimnames = list(c("A", "Bt", "C"), + c("A", "Bt", "C")), ties = FALSE)) + + horizons(sp3)$genhz2 <- NA + expect_message(hzTransitionProbabilities(sp3, "genhz2")) + + # ties in probability matrix + dftest <- data.frame(id = c(1,1,1,2,2,2), + top = c(0,25,50,0,25,50), + bottom = c(25,50,100,25,50,100), + genhz = c("A","B","C","A","B","R")) + depths(dftest) <- id ~ top + bottom + expect_warning(hzTransitionProbabilities(dftest, "genhz")) +}) + +test_that("evalGenHZ works as expected", { + res <- evalGenHZ(sp3, genhz = "genhz", vars = "clay") + expect_equal(names(res), c("horizons","stats","dist")) + expect_equal(res$stats$clay, c("11.26 (3.46)", "28.29 (11.49)", "8.3 (1.74)")) +}) + +test_that("generalize.hz works as expected", { + res <- generalize.hz(sp3$name, new = "H", pat = ".*") + expect_equal(levels(res), c("H","not-used")) + expect_equal(as.character(res)[10], "H") + + res <- generalize.hz(sp3$genhz, new = c("A","Bt"), pat = c("A","[^A]")) + expect_equal(levels(res), c("A","Bt","not-used")) + expect_equal(as.character(res)[10], "Bt") +}) + +test_that("get.ml.hz works as expected", { + res <- get.ml.hz(slab(sp3, fm = ~ genhz, cpm = 1, slab.structure = 0:max(sp3))) + expect_equal(nrow(res), 3) + expect_equal(res$top, c(0,10,60)) + expect_equal(res$confidence, c(50,49,67)) +}) diff --git a/tests/testthat/test-glom.R b/tests/testthat/test-glom.R index 4dda79171..1c7eea80d 100644 --- a/tests/testthat/test-glom.R +++ b/tests/testthat/test-glom.R @@ -5,45 +5,61 @@ depths(sp1) <- id ~ top + bottom site(sp1) <- ~ group p <- sp1[6] -attr <- 'prop' # clay contents % +attr <- 'prop' # clay contents % test_that("intersection of horizons by depth", { # intersection at a single depth should return only one horizon expect_equal(sum(glom(p, 50, ids = TRUE) %in% hzID(p)), 1) - + # intersection from 25 to 100 should return four horizons expect_equal(sum(glom(p, 25, 100, ids = TRUE) %in% hzID(p)), 4) }) +test_that("depthWeights parity", { + # depthWeights calculates contributing fractions within glom intervals for a profile p + wts <- depthWeights(p$top, p$bottom, 25, 100) + + # wrong length errors + expect_error(depthWeights(p$top, p$bottom, c(25,50), 100)) + expect_error(depthWeights(p$top[1], p$bottom, 25, 100)) + + expect_equal(wts, c(0, 0, 0.32, 0.186666666666667, 0.36, 0.133333333333333, 0, 0, 0, 0)) + expect_equal(sum(wts), 1) + glmp <- glom(p, 25, 100, truncate = TRUE) + glmp$thk <- glmp$bottom - glmp$top + glmp$relthk <- glmp$thk / sum(glmp$thk) + expect_equal(glmp$relthk, wts[wts > 0]) +}) + test_that("degenerate single horizon case", { # glom tests with a degenerate case test <- data.frame(id = 1, top = 0, bottom = 100) depths(test) <- id ~ top + bottom - + # default glom expect_silent(t1 <- glom(test, 25, 50)) expect_equal(t1$bottom - t1$top, 100) - + # thickest-modality default glom expect_silent(t1 <- glom(test, 25, 50, modality = "thickest")) expect_equal(t1$bottom - t1$top, 100) - + # truncate glom expect_silent(t1 <- glom(test, 25, 50, truncate = TRUE)) expect_equal(t1$bottom - t1$top, 25) - + # thickest-modality truncate glom expect_silent(t1 <- glom(test, 25, 50, truncate = TRUE, modality = "thickest")) expect_equal(t1$bottom - t1$top, 25) - + # invert glom expect_silent(t1 <- glom(test, 25, 50, invert = TRUE)) expect_equal(t1$bottom - t1$top, 100) - + # invert + truncate glom expect_silent(t1 <- glom(test, 25, 50, invert = TRUE, truncate = TRUE)) expect_equal(t1$bottom - t1$top, c(25, 50)) - + # thickest-modality invert + truncate glom expect_silent(t1 <- glom(test, 25, 50, invert = TRUE, truncate = TRUE, modality = "thickest")) expect_equal(t1$bottom - t1$top, 50) @@ -51,128 +67,128 @@ test_that("degenerate single horizon case", { test_that("glom by depth returns a SPC clod", { # glom 'gloms' your input SPC `p`'s horizons (by depths specified) into a 'clod' - + # currently "clods" can be either represented as an SPC, or a data.frame with just # the horizons that are contained within the "clod". foo <- glom(p, 25, 100) # and returns an SPC expect_true(inherits(foo, 'SoilProfileCollection')) - + # within that SPC there should be only one profile expect_equal(length(foo), 1) - + # and that profile should have 4 horizons in 25-100cm - expect_equal(nrow(foo), 4) + expect_equal(nrow(foo), 4) }) test_that("glom by depth returns a data.frame clod", { # glom 'gloms' your input SPC `p`'s horizons (by depths specified) into a 'clod' - + # currently "clods" can be either represented as an SPC, or a data.frame with just # the horizons that are contained within the "clod". foo <- glom(p, 25, 100, df = TRUE) - + # and returns an data.frame expect_true(inherits(foo, 'data.frame')) - + # within that data.frame, length() returns 18 expect_equal(length(foo), 18) - + # and that data.frame should have 4 horizons (rows) in 25-100cm - expect_equal(nrow(foo), 4) + expect_equal(nrow(foo), 4) }) test_that("glom truncate = TRUE works as expected", { # glom 'gloms' your input SPC `p`'s horizons (by depths specified) into a 'clod' - + # get truncated clod foo <- glom(p, 25, 100, truncate=TRUE) - + # and returns an data.frame expect_true(inherits(foo, 'SoilProfileCollection')) - + ## test that: # shallowest top truncated to z1 # deepest bottom truncated to z2 expect_equal(min(foo$top), 25) expect_equal(max(foo$bottom), 100) - + }) test_that("glomApply works as expected", { - + # using same depth returns a single horizon from every profile # glomApply with same z1 as z2 expect_silent(res <- glomApply(sp1, function(p) return(c(25,25)))) - + # above is the same as calling glom with just z1 specified (for first profile in sp1) - + # there are slight differences in hzID due to glomApply using union internally after glom-ing expect_equivalent(res[1,], glom(sp1[1,], 25)) - + # they are equivalent but not equal expect_error(expect_equal(res[1,], glom(sp1[1,], 25))) - + # after union hzID 3 becomes 1 (since sp1 does not have a true hzID specified) tdepths <- horizonDepths(sp1) expect_equal(horizons(res[1,])[,tdepths], horizons(glom(sp1[1,], 25))[,tdepths]) - + # every profile returns one horizon (all profiles at least 25cm deep) expect_true(all(profileApply(res, nrow) == 1)) - + # glom returns empty results for some of these profiles at 200cm # glom will produce a NULL, and union will drop that profile expect_warning(res2 <- glomApply(sp1, function(p) return(c(200,200)))) - + ## 7 profiles have 200 as an invalid upper bound ## note that P006 and P008 end EXACTLY at 200 and are NOT included # plot(sp1) # abline(h = 200) expect_equal(names(profileApply(res2, nrow)), c("P007", "P009")) - + test_that("realistic glomApply scenarios", { data(sp3) depths(sp3) <- id ~ top + bottom - - # constant depths, whole horizon returns by default - expect_warning(glomApply(sp3, function(p) c(25,100))) - + + # constant depths, whole horizon returns by default + expect_warning(glomApply(sp3, function(p) c(25,100))) + # constant depths, truncated #(see aqp::trunc for helper function) - expect_silent(glomApply(sp3, function(p) c(25,30), truncate = TRUE)) - - # constant depths, inverted + expect_silent(glomApply(sp3, function(p) c(25,30), truncate = TRUE)) + + # constant depths, inverted expect_warning(glomApply(sp3, function(p) c(25,100), invert = TRUE)) - + # constant depths, inverted + truncated (same as above) expect_silent(res <- glomApply(sp3, function(p) c(25,30), invert = TRUE, truncate = TRUE)) - + # before invert, 46 horizons expect_equal(nrow(sp3), 46) - + # after glom invert, some horizons are split, increasing the total number expect_equal(nrow(res), 52) - + # random boundaries in each profile the specific warnings are a product of pseudorandom numbers set.seed(100) expect_warning(glomApply(sp3, function(p) round(sort(runif(2, 0, max(sp3)))))) - + # random boundaries in each profile (truncated) expect_warning(glomApply(sp3, function(p) round(sort(runif(2, 0, max(sp3)))), truncate = TRUE)) - + # calculate some boundaries as site level attribtes expect_warning(sp3$glom_top <- profileApply(sp3, getMineralSoilSurfaceDepth)) expect_silent(sp3$glom_bottom <- profileApply(sp3, estimateSoilDepth)) - + # use site level attributes for glom intervals for each profile expect_silent(glomApply(sp3, function(p) return(c(p$glom_top, p$glom_bottom)))) }) - + # trunc wrapper function for constant depths works as expected - expect_equal({ - glomApply(sp1, function(p) return(c(25,36)), truncate = TRUE) - },{ - trunc(sp1, 25, 36) + expect_equal({ + glomApply(sp1, function(p) return(c(25,36)), truncate = TRUE) + },{ + trunc(sp1, 25, 36) }) }) diff --git a/tests/testthat/test-mollic.R b/tests/testthat/test-mollic.R new file mode 100644 index 000000000..8d1e458e1 --- /dev/null +++ b/tests/testthat/test-mollic.R @@ -0,0 +1,22 @@ +context("estimate mollic epipedon bounds") +# construct a fake profile +spc <- data.frame(id=1, taxsubgrp = "Lithic Haploxeralfs", + hzname = c("A","AB","Bt","BCt","R"), + hzdept = c(0, 20, 32, 42, 49), + hzdepb = c(20, 32, 42, 49, 200), + prop = c(18, 22, 28, 24, NA), + texcl = c("l","l","cl", "l","br"), + d_value = c(5, 5, 5, 6, NA), + m_value = c(2.5, 3, 3, 4, NA), + m_chroma = c(2, 3, 4, 4, NA)) + +# promote to SoilProfileCollection +depths(spc) <- id ~ hzdept + hzdepb +hzdesgnname(spc) <- 'hzname' +hztexclname(spc) <- 'texcl' + +test_that("mollic.thickness.requirement", { + expect_equal(mollic.thickness.requirement(spc, clay.attr = 'prop'), 18) + expect_equal(mollic.thickness.requirement(spc, clay.attr = 'prop', truncate = FALSE), 49 / 3) + expect_equal(mollic.thickness.requirement(trunc(spc, 0, 9), clay.attr = 'prop'), 10) +}) diff --git a/tests/testthat/test-plotSPC.R b/tests/testthat/test-plotSPC.R index 7c39552b2..df057e44f 100644 --- a/tests/testthat/test-plotSPC.R +++ b/tests/testthat/test-plotSPC.R @@ -12,20 +12,20 @@ site(sp1) <- ~ group ## tests test_that("plotSPC: aqp.env settings", { - + # explainer explainPlotSPC(sp1) - + # get plotting details from aqp environment lsp <- get('last_spc_plot', envir=aqp.env) - + # should be a list expect_true(is.list(lsp)) - + # check for required components - expect_equal(names(lsp), c("width", "plot.order", "x0", "pIDs", "idname", "y.offset", "scaling.factor", + expect_equal(names(lsp), c("width", "plot.order", "x0", "pIDs", "idname", "y.offset", "scaling.factor", "max.depth", "n", "extra_x_space", "extra_y_space")) - + # basic integrity checks expect_equal(profile_id(sp1), lsp$pIDs) expect_equal(idname(sp1), lsp$idname) @@ -34,11 +34,11 @@ test_that("plotSPC: aqp.env settings", { }) test_that("plotSPC: figure settings", { - + # explainer returns `lsp` - lsp <- explainPlotSPC(sp1, scaling.factor=0.5, width=0.8, + lsp <- explainPlotSPC(sp1, scaling.factor=0.5, width=0.8, y.offset=8, n=15, max.depth = 100) - + # check adjustments expect_equal(lsp$scaling.factor, 0.5) expect_equal(lsp$width, 0.8) @@ -49,30 +49,30 @@ test_that("plotSPC: figure settings", { test_that("plotSPC: re-ordering of profiles", { - + # re-order new.order <- c(1,3,5,7,9,2,4,6,8) # explainer returns `lsp` lsp <- explainPlotSPC(sp1, plot.order=new.order) - + # profile IDs should be sorted according to plot.order expect_equal(profile_id(sp1)[new.order], lsp$pIDs) - + # plotting order should be saved expect_equal(new.order, lsp$plot.order) }) test_that("plotSPC: relative spacing", { - + # relative positions x.pos <- c(1+0, 2+0, 3+0, 4+0.1, 5+0.1, 6+0.1, 7+0.2, 8+0.2, 9+0.2) # explainer returns `lsp` lsp <- explainPlotSPC(sp1, relative.pos = x.pos) - + # plot order not affected expect_equal(lsp$plot.order, 1:length(sp1)) - + # x0 adjusted as expected # 1:length(x) + offsets expect_equal(lsp$x0, x.pos) @@ -82,90 +82,111 @@ test_that("plotSPC: relative spacing", { test_that("plotSPC: re-ordering of profiles and relative spacing", { - + # new order new.order <- c(1,3,5,7,9,2,4,6,8) - # relative positions: in the new ordering + # relative positions: in the new ordering x.pos <- c(1+0, 2+0, 3+0, 4+0.1, 5+0.1, 6+0.1, 7+0.2, 8+0.2, 9+0.2) - + # explainer returns `lsp` lsp <- explainPlotSPC(sp1, plot.order=new.order, relative.pos = x.pos) - + # profile IDs should be sorted according to plot.order expect_equal(profile_id(sp1)[new.order], lsp$pIDs) - + # plotting order should be saved expect_equal(new.order, lsp$plot.order) - + # x0 adjusted as expected expect_equal(lsp$x0, x.pos) }) test_that("plotSPC: re-ordering via relative spacing", { - + # re-order by adjusting the relative positions x.pos <- length(sp1):1 - + # explainer returns `lsp` lsp <- explainPlotSPC(sp1, relative.pos = x.pos) - + # plotting order / IDs are not modified! expect_equal(profile_id(sp1), lsp$pIDs) expect_equal(lsp$plot.order, 1:length(sp1)) - + # x0 adjusted as expected expect_equal(lsp$x0, x.pos) }) +test_that("addBracket works", { + + expect_silent(addBracket(data.frame(id = profile_id(sp1), label="bar", top = 0, bottom = 25))) + + expect_silent(addBracket(data.frame(id = profile_id(sp1), top = 0, bottom = NA))) + + diagnostic_hz(sp1) <- data.frame(id = profile_id(sp1), featkind = "foo", featdept = 0, featdepb = 50) + + expect_silent(addDiagnosticBracket(sp1, kind = "foo")) + +}) test_that("addVolumeFraction works", { - + # does it work with default arguments? - plotSPC(sp1, name='name') + plotSPC(sp1, name = 'name') + expect_silent(addVolumeFraction(sp1, 'prop')) - + # additional arguments expect_silent(addVolumeFraction(sp1, 'prop', res = 5)) - + expect_silent(addVolumeFraction(sp1, 'prop', cex.min = 0.5, cex.max = 2)) - + expect_silent(addVolumeFraction(sp1, 'prop', pch = 15)) - + # color specification is important: # single color expect_silent(addVolumeFraction(sp1, 'prop', col = 'red')) - + # or vector of colors, must be same length as nrow(x) expect_silent(addVolumeFraction(sp1, 'prop', col = rep('green', times=nrow(sp1)))) + sp1$prop1k <- sp1$prop / 1000 + # message due to values < 1 + expect_message(addVolumeFraction(sp1, 'prop1k'), + "all prop1k values are < 0.5, likely a fraction vs. percent") + + sp1$prop1k <- sp1$prop * 1000 + # warnings due to values >100 + expect_warning(addVolumeFraction(sp1, 'prop1k')) + }) test_that("addVolumeFraction expected errors", { - + plotSPC(sp1, name='name') - + # bad column name expect_error(addVolumeFraction(sp1, 'prop1')) - + # incorrectly specified colors expect_error(addVolumeFraction(sp1, 'prop', col = c('red', 'green'))) - + }) # https://github.com/ncss-tech/aqp/issues/8 test_that("addVolumeFraction fractional horizon depths", { - + plotSPC(sp1, name='name') - - + + # modify depths sp1$top[4] <- sp1$top[4] + 0.5 sp1$bottom[3] <- sp1$top[4] - + # fractional horizon depths expect_message(addVolumeFraction(sp1, 'prop'), regexp = 'truncating') - + }) diff --git a/tests/testthat/test-profileApply.R b/tests/testthat/test-profileApply.R index 6562fb9ba..6e0cafd76 100644 --- a/tests/testthat/test-profileApply.R +++ b/tests/testthat/test-profileApply.R @@ -1,20 +1,20 @@ -context("profileApply - SoilProfileCollection iterator") +context("SoilProfileCollection iterator (profileApply") data(sp1, package = 'aqp') depths(sp1) <- id ~ top + bottom site(sp1) <- ~ group -attr <- 'prop' # clay contents % +attr <- 'prop' # clay contents % test_that("profileApply - basic tests of output datatypes", { r1 <- profileApply(sp1, estimateSoilDepth, name="name", top="top", bottom="bottom") expect_equal(names(r1),c("P001", "P002", "P003", "P004", "P005", "P006", "P007", "P008", "P009")) expect_equal(r1, c(P001 = 89L, P002 = 59L, P003 = 67L, P004 = 62L, P005 = 68L, P006 = 200L, P007 = 233L, P008 = 200L, P009 = 240L)) - + r2 <- profileApply(sp1, estimateSoilDepth, name="name", top="top", bottom="bottom", simplify = FALSE) - expect_equal(r2, list(P001 = 89L, P002 = 59L, P003 = 67L, P004 = 62L, P005 = 68L, + expect_equal(r2, list(P001 = 89L, P002 = 59L, P003 = 67L, P004 = 62L, P005 = 68L, P006 = 200L, P007 = 233L, P008 = 200L, P009 = 240L)) - + r3 <- profileApply(sp1, function(p) { d <- estimateSoilDepth(p, name="name", top="top", bottom="bottom") res <- data.frame(profile_id(p), d, stringsAsFactors = FALSE) @@ -23,3 +23,35 @@ test_that("profileApply - basic tests of output datatypes", { }, frameify = TRUE) expect_true(inherits(r3,'data.frame')) }) + +test_that("profileApply - frameify option", { + + # 1 row per profile + expect_silent(r1 <- profileApply(sp1, frameify = TRUE, column.names = c("foo","bar"), function(p) { + data.frame(id = profile_id(p), + depth = estimateSoilDepth(p, name = "name", top = "top", bottom = "bottom")) + })) + expect_equal(nrow(r1), 9) + + # 1 row per horizon + expect_silent(r1 <- profileApply(sp1, frameify = TRUE, column.names = c("foo","bar"), function(p) { + data.frame(id = profile_id(p), + hzID = hzID(p), + depth = estimateSoilDepth(p, name = "name", top = "top", bottom = "bottom")) + })) + expect_equal(nrow(r1), 60) + + # some profiles with no result + expect_silent(r2 <- profileApply(sp1, frameify = TRUE, function(p) { + res <- data.frame(id = profile_id(p), + depth = estimateSoilDepth(p, name = "name", top = "top", bottom = "bottom")) + if (res$depth < 200) + return(res) + })) + + # non-data.frame first result + expect_warning(r2 <- profileApply(sp1[1,], frameify = TRUE, function(p) { + estimateSoilDepth(p, name = "name", top = "top", bottom = "bottom") + })) + expect_true(inherits(r2, "list")) +}) diff --git a/tests/testthat/test-pscs.R b/tests/testthat/test-pscs.R index b77a89239..97277b12f 100644 --- a/tests/testthat/test-pscs.R +++ b/tests/testthat/test-pscs.R @@ -7,22 +7,61 @@ depths(sp1) <- id ~ top + bottom site(sp1) <- ~ group p <- sp1[1] -attr <- 'prop' # clay contents % +attr <- 'prop' # clay contents % q <- sp1[2] test_that("estimatePSCS()", { - + # this soil has a clay decrease then a clay increase and an argillic horizon # the argillic horizon ends at a bedrock contact - e <- estimatePSCS(p, clay.attr='prop', texcl.attr="texture", hzdesgn='name') + e <- estimatePSCS(p, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') expect_equivalent(e, c(49, 89)) - + # this soil does not have an argillic, so it is 25-100 but has 5cm thick O horizon # and is moderately deep to bedrock contact - g <- estimatePSCS(q, clay.attr='prop', texcl.attr="texture", hzdesgn='name') + g <- estimatePSCS(q, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') expect_equivalent(g, c(30, 59)) - + + ## special cases + + # thick (>50cm) Bt + qbigbt <- sp1[3] + qbigbt$name <- c("A","Bt1","Bt2","2Bt3", "2Bt4") + g <- estimatePSCS(qbigbt, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') + expect_equivalent(g, c(2, 52)) + + # thick Ap (>25cm) + qbigap <- sp1[3] + qbigap$name <- c("Ap1","Ap2","Ap3","C1", "C2") + g <- estimatePSCS(qbigap, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') + expect_equivalent(g, c(35, 67)) + + # soil less than 36cm deep + qshallow <- trunc(q, 0, 27) + g <- estimatePSCS(qshallow, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') + expect_equivalent(g, c(5, 27)) + + # andisols + qandisol <- q + qandisol$tax_order <- "Andisols" + g <- estimatePSCS(qandisol, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') + expect_equivalent(g, c(5, 59)) # note: starts at bottom of OSM + + # very shallow (bottom depth <25cm) argillic? + qminiargi <- sp1[3] + qminiargi$name <- c("A","Bt","C1","2C2", "2C3") #idk... + g <- estimatePSCS(qminiargi, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'name') + expect_equivalent(g, c(2, 67)) # NOT 2, 13; would be 2,100 without limiting layer + + # error conditions + q2 <- q + expect_error(estimatePSCS(q2, clay.attr = 'foo', texcl.attr = "texture", hzdesgn = 'name')) + q2$texture <- NULL + expect_error(estimatePSCS(q2, clay.attr = 'prop', texcl.attr = "foo", hzdesgn = 'name')) + q2 <- q + q2$name <- NULL + expect_error(estimatePSCS(q2, clay.attr = 'prop', texcl.attr = "texture", hzdesgn = 'foo')) }) diff --git a/tests/testthat/test-sim.R b/tests/testthat/test-sim.R index daa03c40b..9ca1f5aef 100644 --- a/tests/testthat/test-sim.R +++ b/tests/testthat/test-sim.R @@ -14,18 +14,18 @@ s$name <- paste('H', seq_along(s$name), sep = '') ## tests test_that("sim() works as expected", { - + # simulate 25 new profiles expect_message({sim.1 <- sim(s, n = 25)}, "converting profile IDs from integer to character") - + expect_message({sim.2 <- sim(s, n = 25, hz.sd = c(1, 2, 5, 5, 5, 10, 3))}, "converting profile IDs from integer to character") - + # result is an SPC expect_true(inherits(sim.1, 'SoilProfileCollection')) expect_true(inherits(sim.2, 'SoilProfileCollection')) - + # expected lengths expect_true(length(sim.1) == 25) expect_true(length(sim.2) == 25) @@ -33,13 +33,31 @@ test_that("sim() works as expected", { test_that("expected errors", { - + # only 1 seed can be used expect_error(sim(sp3[1:2, ], n = 25)) - + # sd must recycle evenly over number of original horizons # NOTE: now following numeric id order for numeric id expect_error(sim(s, n = 25, hz.sd = 1:4)) - + +}) + +test_that("permute_profile() works as expected", { + # simulate 25 new profiles with a sd boundary thickness of 0.5 - 2.5cm + s$bdy <- round(runif(nrow(s), 1, 5)) / 2 + diagnostic_hz(s) <- data.frame(id = profile_id(s), + featkind = "foo", + featdept = 0, featdepb = 10) + restrictions(s) <- data.frame(id = profile_id(s), + restrkind = "bar", + restrdept = 0, restrdepb = 10) + perp <- permute_profile(s, n = 25, "bdy") + + # result is an SPC + expect_true(inherits(perp, 'SoilProfileCollection')) + + # expected lengths + expect_true(length(perp) == 25) }) diff --git a/tests/testthat/test-soil-depth.R b/tests/testthat/test-soil-depth.R index 973606a0a..0a6db8971 100644 --- a/tests/testthat/test-soil-depth.R +++ b/tests/testthat/test-soil-depth.R @@ -1,7 +1,7 @@ context("soil depth estimation") ## sample data -d <- +d <- rbind( data.frame( id = c(1, 1, 1), @@ -35,10 +35,10 @@ depths(d) <- id ~ top + bottom ## tests test_that("error conditions", { - + # function will only accept a single profile expect_error(estimateSoilDepth(d, name='name', top='top', bottom='bottom')) - + # required column names: name, top, bottom not specified, defaults not appropriate # AGB: now capable of basic guessing of hzdesgn, and uses hzdepthcol slot internally # expect_error(estimateSoilDepth(d[1, ])) @@ -46,59 +46,85 @@ test_that("error conditions", { test_that("basic soil depth evaluation, based on pattern matching of hz designation", { - - # setting hz desgn by argument works + + # setting hz desgn by argument works res <- profileApply(d, estimateSoilDepth, name='name') expect_equivalent(res, c(110, 55, 48, 20)) - + # setting hz desgn by argument works by guessing hzname res <- profileApply(d, estimateSoilDepth) expect_equivalent(res, c(110, 55, 48, 20)) - + # setting nonexistent hzdesgn produces no error (by guessing hzname) res <- profileApply(d, estimateSoilDepth, name='goo') expect_equivalent(res, c(110, 55, 48, 20)) - + # remove the guessable name, expect error d$xxx <- d$name d$name <- NULL expect_error(estimateSoilDepth(d[1,], name='name')) - + # backup use of S4 hzdesgncol slot in lieu of valid argument/guessable name column hzdesgnname(d) <- "xxx" - res <- estimateSoilDepth(d[1,], name='name') + res <- estimateSoilDepth(d[1,], name='name') expect_equivalent(res, 110) }) test_that("application of reasonable depth assumption of 150, given threshold of 100", { - + res <- profileApply(d, estimateSoilDepth, no.contact.depth=100, no.contact.assigned=150) - + expect_equivalent(res, c(150, 55, 48, 20)) }) test_that("depth to feature using REGEX on hzname: [Bt]", { - + # example from manual page, NA used when there is no 'Bt' found res <- profileApply(d, estimateSoilDepth, p='Bt', no.contact.depth=0, no.contact.assigned=NA) - + expect_equivalent(res, c(20, 20, 20, NA)) }) +test_that("depthOf - simple match", { + expect_equal(depthOf(d[1,], "Cr|R|Cd"), NA) + expect_equal(depthOf(d[2,], "Cr|R|Cd"), 55) + expect_equal(minDepthOf(d[2,], "Cr|R|Cd"), 55) + expect_equal(maxDepthOf(d[2,], "Cr|R|Cd"), 55) + expect_equal(maxDepthOf(d[2,], "Cr|R|Cd", top = FALSE), 80) +}) + +test_that("depthOf - multiple match", { + expect_equal(depthOf(d[1,], "A|B|C"), c(0,20,35)) + expect_equal(depthOf(d[1,], "A|B|C", top = FALSE), c(20,35,110)) + expect_equal(minDepthOf(d[1,],"A|B|C"), 0) + expect_equal(maxDepthOf(d[1,],"A|B|C"), 35) + expect_equal(minDepthOf(d[1,], "A|B|C", top = FALSE), 20) + expect_equal(maxDepthOf(d[1,], "A|B|C", top = FALSE), 110) +}) + +test_that("depthOf - no match", { + expect_equal(depthOf(d[1,], "X"), NA) + expect_equal(depthOf(d[2,], "Cr|R|Cd", no.contact.depth = 50), NA) + + d2 <- d + d2$name <- NULL + expect_error(depthOf(d2[1,], "A|B|C")) +}) + test_that("soil depth class assignment, using USDA-NRCS class breaks", { - + res <- getSoilDepthClass(d) - + # result should be a data.frame with as many rows as profiles in input expect_true(inherits(res, 'data.frame')) expect_equal(nrow(res), length(d)) - + # depths, should be the same as prior tests using estimateSoilDepth expect_equivalent(res$depth, c(110, 55, 48, 20)) - + # depth classes are returned as a factor sorted from shallow -> deep dc <- factor(c('deep', 'mod.deep', 'shallow', 'very.shallow'), levels=c('very.shallow', 'shallow', 'mod.deep', 'deep', 'very.deep')) expect_equivalent(res$depth.class, dc)