Nothing
#' Effect level
#'
#' Derives the effect level due to toxicant exposure in the supplied scenarios.
#' Either relative to a control scenario or derived directly from model
#' endpoints, depending on model type. For scenarios with moving exposure windows,
#' the maximum effect is returned.
#'
#' By default, only the maximum effect in all moving exposure windows will
#' be returned. If argument `max_only=FALSE` is set, the returned table will
#' be converted to long-format and will contain effect levels for each
#' assessed exposure window.
#'
#' ## Calculation
#'
#' Effects are calculated similarly to *relative errors*, i.e. the difference
#' between control and treatment scenarios is divided by the absolute value
#' of the control. Effects are usually in the interval `[0,1]`, but values
#' larger than one or smaller than zero can occur. As a special case, if the
#' endpoint from the control scenario is zero, then the effect is either
#' - zero, if also the treatment is zero
#' - positive infinity, if the treatment is smaller than zero
#' - negative infinity, if the treatment is greater than zero
#'
#' As an example, a control scenario achieves a biomass of
#' 1.0 and the treatment scenario achieves a biomass of 0.9, the effect will
#' be equal to 0.1 or 10%. However, effects can take on any real value. If,
#' for example, the biomass of the previously mentioned treatment scenario drops
#' below zero, then an effect larger than 1.0 will be calculated If, instead,
#' the biomass in the treatment scenario is greater than in the control, then the
#' effect will be negative.
#'
#' ### Output formatting
#' Start and end time of exposure windows can be disabled by setting `ep_only=TRUE`.
#' Effect levels smaller than a certain threshold can be automatically set to
#' zero (`0.0`) to avoid spurious effect levels introduced by numerical errors.
#' Set `marginal_effect` to an adequate value less than 1%.
#'
#' ### Computational efficiency
#' Calculations can be sped up by providing a `data.frame` of pre-calculated
#' control scenarios for each assessed time window. As control scenarios are
#' by definition independent of any exposure multiplication factor, they can
#' be reused for repeated calculations, e.g. to derive effect profiles or
#' dose-response relationships.
#'
#' @param x a [scenario] objects
#' @param factor optional numeric value which scales the exposure time-series
#' @param ep_only logical, if TRUE only effect endpoints are returned as a vector
#' @param max_only `logical`, if `TRUE` only the maximum effect is returned, else
#' results for all effect windows are reported
#' @param marginal_effect `numeric`, if set, any effect smaller than this threshold will
#' be reported as zero to exclude pseudo-effects originating from small numerical
#' errors
#' @param ... additional parameters passed on to [simulate()]
#'
#' @return a `tibble`, by default containing scenarios, effect levels, and the
#' exposure window where the maximum effect level occurred. The number of columns
#' depends on the enabled effect endpoints and function arguments.
#'
#' By default, the first column, named `scenarios`, contains the original scenario
#' objects that were the basis of the calculation. For each effect endpoint, it
#' will be followed by one column with the maximum effect level and two columns
#' containing start and end time of the associated exposure window. If exposure
#' windows are disabled, the columns will just contain the start and end time of
#' the simulation. The effect level column will have the name of the effect
#' endpoint, start and end time will additionally have the suffixes `.dat.start`
#' and `.dat.end`, respectively.
#' @export
setGeneric("effect", function(x, ...) standardGeneric("effect"), signature="x")
#' @autoglobal
effect_scenario <- function(x, factor=1, max_only=TRUE, ep_only=FALSE, marginal_effect, .cache, ...) {
if(is.vector(x))
stop("vectors of scenarios not supported")
eps <- get_endpoints(x)
if(length(eps) == 0)
warning("no endpoints selected", call.=FALSE)
# apply multiplication factor
if(is_sequence(x)) {
for(i in seq_along(x))
x[[i]]@exposure@series[, 2] <- x[[i]]@exposure@series[, 2] * factor
} else {
x@exposure@series[, 2] <- x@exposure@series[, 2] * factor
}
# is a cache set? it would contain window candidates as well as results of
# control simulations (i.e. with zero exposure)
if(missing(.cache)) {
.cache <- cache_windows(x, ...)
}
ctrl_req <- is_control_required(x)
# calculate effects for all windows
efx <- do.call(rbind, lapply(seq_along(.cache$windows), function(i) {
# current exposure window
win <- .cache$windows[[i]]
# derive endpoints rel to ctrl if required, otherwise use values as they are
rs <- fx(clip_scenario(x, window=win), ...)
if(ctrl_req) {
rs <- calc_effect(rs, .cache$controls[[i]])
}
# make sure all endpoints exist
missing <- eps[is.na(rs[eps])]
if(length(missing) > 0) {
rs[missing] <- NA_real_
}
# return effects including exposure window
c(win, rs)
}))
# set effects with an absolute value smaller than the marginal effect
# threshold to zero
if(!missing(marginal_effect)) {
if(marginal_effect > 0.01) {
warning("marginal effect threshold is larger than 1%")
}
efx[, eps] <- ifelse(abs(efx[, eps]) < marginal_effect, 0, efx[, eps])
}
# find maximum of all endpoints and corresponding time points
if(max_only) {
# check if all endpoints are present in result set
#if(length(setdiff(eps, names(efx))) > 0)
# stop(paste("endpoint(s)",paste(setdiff(eps, names(efx)),collapse=","),"missing in effect result"))
rs <- list()
for(ep in eps) {
idx <- which.max(efx[, ep])
# idx can be empty if all endpoint values are NA, make sure we select some row
idx <- ifelse(length(idx) == 0, 1, idx)
max.efx <- efx[idx, c(ep, "window.start", "window.end")]
names(max.efx) <- c(ep, paste0(ep, ".dat.start"), paste0(ep, ".dat.end"))
rs <- append(rs, max.efx)
}
# discard timestamps and scenario?
if(ep_only)
rs <- unlist(rs[eps])
else {
rs <- append(list(scenario=list(x)), rs)
rs <- tibble::as_tibble(rs)
}
return(rs)
}
# return endpoints only, for all windows
else if(ep_only) {
return(as.data.frame(subset(efx, select=eps)))
}
# rename date columns, move to back
efx <- dplyr::relocate(as.data.frame(efx), window.start, window.end, .after=dplyr::last_col()) %>%
dplyr::rename(dat.start=window.start, dat.end=window.end)
# add scenario object
efx$scenario <- list(x)
dplyr::relocate(efx, scenario)
}
#' @describeIn effect Default for all generic [scenarios]
#' @include class-EffectScenario.R
#' @export
setMethod("effect", "EffectScenario", function(x, factor=1, max_only=TRUE, ep_only=FALSE, marginal_effect, ...)
effect_scenario(x, factor=factor, max_only=max_only, ep_only=ep_only, marginal_effect=marginal_effect, ...))
#setMethod("effect", "EffectScenario", effect_scenario)
#' @describeIn effect For scenario [sequences][sequence]
#' @include sequence.R
#' @export
setMethod("effect", "ScenarioSequence", function(x, ...) effect_scenario(x, ...))
# Calculates the effect based on simulation and control. Takes into
# account some edge cases where sim/control are zero which would otherwise
# introduce NaNs.
#
# @param sim Usually a scenario with non-zero exposure, i.e. a treatment
# @param control A scenario with zero exposure
# @return Any real value
calc_effect <- function(sim, control) {
rs <- (control - sim) / abs(control) # effect relative to control
# handle zeroes in control endpoints
idx_c0 <- control == 0
if(any(idx_c0, na.rm=TRUE)) {
rs[idx_c0] <- -sign(sim[idx_c0]) * Inf # infinite effect if sim <> 0 but control is zero
rs[idx_c0 & sim == 0] <- 0 # no effect if control and sim are zero
}
rs
}
#### Helper functions ####
clip_scenario <- function(scenario, window) {
if(missing(window))
stop("nothing to clip")
if(length(window) != 2)
stop("window must consist of a start and end time point")
if(any(is.na(window)))
stop("window contains invalid values")
# select relevant range
times <- get_times(scenario)
times <- times[times >= min(window) & times <= max(window)]
# make sure that start and end of window are included
if(length(times) == 0)
times <- window[[1]]
else if(times[1] > window[1])
times <- c(window[[1]], times)
if(tail(times,1) < window[2])
times <- c(times, window[[2]])
# update output times
scenario <- set_times(scenario, times)
# do not touch exposure & forcing time-series of sequences, it's too fragile
if(is_sequence(scenario)) {
return(scenario)
}
# clip exposure series
exposure <- clip_forcings(scenario@exposure@series, window)
# clip any forcings data
forcings <- scenario@forcings
for(fnm in names(forcings))
forcings[[fnm]] <- clip_forcings(forcings[[fnm]], window)
# update other scenario settings
scenario@exposure@series <- exposure
scenario@forcings <- forcings
scenario
}
#
# Minimze forcing time-series to reduce numerical overhead in deSolve
#
# It's okay if the resulting data.frame does not (fully) cover the requested
# window as forcings will be interpolated and extended by the solver as needed.
# Because of subtle interpolation issues, we also have to include the row just
# before and after our window of interest.
#
clip_forcings <- function(df, window) {
if(nrow(df)<=2)
return(df)
df[seq(max(1,sum(df[,1]<window[1])), min(nrow(df),sum(df[,1]<=window[2])+1)),]
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.