Skip to content

Commit

Permalink
improve unit test coverage #143
Browse files Browse the repository at this point in the history
  • Loading branch information
brownag committed Aug 2, 2020
1 parent 46b9d02 commit c78c408
Show file tree
Hide file tree
Showing 36 changed files with 1,038 additions and 631 deletions.
5 changes: 2 additions & 3 deletions R/AAAA.R
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 5 additions & 5 deletions R/Class-SoilProfileCollection.R
Original file line number Diff line number Diff line change
Expand Up @@ -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 <- ""

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
153 changes: 57 additions & 96 deletions R/SoilProfileCollection-iterators.R
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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))
Expand All @@ -29,115 +29,115 @@ 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')
#'
#' # scale a single property 'prop' in horizon table
#' # 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) {
Expand All @@ -150,112 +150,73 @@ 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)
# then, concatenate the result into a list (or vector if simplify == TRUE)
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)
})
6 changes: 2 additions & 4 deletions R/SoilProfileCollection-methods.R
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 4 additions & 6 deletions R/SoilProfileCollection-spatial.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit c78c408

Please sign in to comment.