From 296ce3cbed5b670ffd2bffdbc3fbcfbba68e4753 Mon Sep 17 00:00:00 2001
From: Sebastian Henz <sebastian.henz@ufz.de>
Date: Sat, 15 Feb 2020 14:29:49 +0100
Subject: [PATCH] Major rework of the source code and documentation. Refactor
 everything to improve usability and to make debugging and extending the
 package easier. See NEWS.md for details.

---
 DESCRIPTION                                   |   4 +-
 NAMESPACE                                     |   3 +-
 NEWS.md                                       |  17 +
 R/ec.R                                        | 104 ++--
 R/ecxsys.R                                    | 521 +++++++-----------
 R/helpers.R                                   |  59 ++
 R/moving_weighted_mean.R                      |  16 -
 R/plot_ecxsys.R                               |  67 +--
 R/plot_effect.R                               | 102 ++--
 R/{plot_system_stress.R => plot_stress.R}     |  36 +-
 R/predict_ecxsys.R                            | 122 ++++
 R/stress_effect_conversion.R                  |  15 +-
 R/stressaddition-package.R                    |   9 +-
 README.md                                     |   9 +-
 man/Stressconversion.Rd                       |  11 +-
 man/ec.Rd                                     |  49 +-
 man/ecxsys.Rd                                 |  75 +--
 man/plot_ecxsys.Rd                            |  21 +-
 man/predict_ecxsys.Rd                         |  59 ++
 tests/testthat/test-ec.R                      |  75 ++-
 tests/testthat/test-ecxsys.R                  | 250 ++++-----
 .../testthat/test-stress_effect_conversion.R  |   8 +-
 22 files changed, 856 insertions(+), 776 deletions(-)
 create mode 100644 R/helpers.R
 delete mode 100644 R/moving_weighted_mean.R
 rename R/{plot_system_stress.R => plot_stress.R} (62%)
 create mode 100644 R/predict_ecxsys.R
 create mode 100644 man/predict_ecxsys.Rd

diff --git a/DESCRIPTION b/DESCRIPTION
index 45b797f..6ca5088 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,8 +1,8 @@
 Package: stressaddition
 Type: Package
 Title: Modeling Tri-Phasic Concentration-Response Relationships
-Version: 1.11.1
-Date: 2020-02-13
+Version: 2.0.0
+Date: 2020-02-15
 Authors@R: c(person("Sebastian", 
                     "Henz", 
                     role = c("aut", "cre"), 
diff --git a/NAMESPACE b/NAMESPACE
index 569bc2a..06741e7 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -4,7 +4,8 @@ export(ec)
 export(ecxsys)
 export(effect_to_stress)
 export(plot_effect)
-export(plot_system_stress)
+export(plot_stress)
+export(predict_ecxsys)
 export(stress_to_effect)
 import(grDevices)
 import(graphics)
diff --git a/NEWS.md b/NEWS.md
index e7d2e27..76e2ec2 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,20 @@
+# stressaddition 2.0.0
+
+* Changed the order of arguments in `ecxsys()`.
+* Removed `hormesis_index` argument from `ecxsys()`. Use `hormesis_concentration` instead.
+* New function `predict_ecxsys()` replaces `fn()` from the `ecxsys()` output.
+* Renamed the arguments in `ec()`.
+* Made `ec()` more flexible. It now also accepts a data.frame with a concentration column and a column of response values.
+* Added LL5 curves to the legend of `plot_effect()`.
+* Replaced every occurrence of "simple" in variable names with "LL5".
+* Replaced every occurrence of "sys_stress" in variable names with "sys" because the extra "stress" was redundant.
+* Renamed `plot_system_stress()` to `plot_stress()` because it is planned to plot more stresses with this function in a future update.
+* Changed the order of the columns in the output of `predict_ecxsys()`.
+* Improved the internal structure of the package.
+* Improved the tests.
+* Improved the documentation.
+
+
 # stressaddition 1.11.1
 
 * First public version.
diff --git a/R/ec.R b/R/ec.R
index 650e249..de29753 100644
--- a/R/ec.R
+++ b/R/ec.R
@@ -1,56 +1,94 @@
 #' Effect Concentrations
 #'
-#' Estimate the concentration to reach a certain effect relative to the control.
+#' Estimate the concentration to reach a certain effect or stress level relative
+#' to the control.
 #'
 #' If the response level occurs multiple times because of hormesis, which may
-#' happen for low values of \code{target_effect}, then the occurrence with the
+#' happen for low values of \code{response_level}, then the occurrence with the
 #' smallest concentration is returned.
 #'
-#' @param model The object returned from \code{ecxsys()}.
-#' @param effect_name The name of the effect for which you want to calculate the
-#'   EC. Must be the name of a column in \code{model$curves}.
-#' @param target_effect The desired effect percentage between 0 and 100. For
-#'   example with the value 10 the function will return the EC10, i.e. the
-#'   concentration where the response falls below 90 \% of the maximum possible
+#' @param model This can be one of three types of objects: Either the output of
+#'   \code{\link{ecxsys}} or the output of \code{\link{predict_ecxsys}} or a
+#'   data frame with a "concentration" column and a \code{response_name} column.
+#'   See the examples.
+#' @param response_name The name of the effect or stress for which you want to
+#'   calculate the EC. Must be one of \code{colnames(model$curves)}.
+#' @param response_level The desired response level as a percentage between 0
+#'   and 100. For example with the value 10 the function will return the EC10,
+#'   i.e. the concentration where the response falls below 90 \% of the maximum
 #'   response.
 #'
-#' @return A list containing the effect concentration and the corresponding
-#'   effect.
+#' @return A list containing the response concentration and the corresponding
+#'   response value.
 #'
-#' @examples model <- ecxsys(
+#' @examples # Calculate the EC_10, the concentration where the effect falls
+#' # below 90 % of the effect in the control.
+#'
+#' model <- ecxsys(
 #'     concentration = c(0, 0.03, 0.3, 3, 10),
 #'     effect_tox_observed = c(85, 76, 94, 35, 0),
-#'     effect_tox_env_observed = c(24, 23, 32, 0, 0),
 #'     hormesis_concentration = 0.3
 #' )
-#' # Calculate the EC_10, the concentration where the effect falls
-#' # below 90 % of the effect in the control:
-#' ec_10 <- ec(model, "effect_tox_sys", 10)
+#'
+#' # using the ecxsys() output or the curves therein directly:
+#' ec(model, "effect_tox_sys", 10)
+#' ec(model$curves, "effect_tox_sys", 10)
+#'
+#' # using the output of predict_ecxsys() with custom concentrations:
+#' conc <- 10^seq(-9, 1, length.out = 1000)
+#' curves <- predict_ecxsys(model, conc)
+#' ec(curves, "effect_tox_sys", 10)
+#'
+#' # using a custom data frame:
+#' df_custom <- data.frame(
+#'     concentration = curves$concentration,
+#'     foo = curves$effect_tox_sys
+#' )
+#' ec(df_custom, "foo", 10)
 #'
 #' @export
-ec <- function(model, effect_name, target_effect) {
+ec <- function(model, response_name, response_level) {
+    if (inherits(model, "drc")) {
+        stop("Please use drc::ED for drc objects.")
+    }
+
     stopifnot(
-        is.character(effect_name),
-        length(effect_name) == 1,
-        effect_name %in% names(model$curves),
-        effect_name != "concentration",
-        target_effect > 0,
-        target_effect < 100
+        is.character(response_name),
+        length(response_name) == 1,
+        response_name != "concentration",
+        response_level > 0,
+        response_level < 100
     )
-    effect <- model$curves[, effect_name]
-    concentration <- model$curves$concentration
-    control_effect <- effect[1]
-    if (control_effect == 0) {
-        stop("Reference effect is zero, calculation of EC not possible.")
+
+    if (!inherits(model, c("ecxsys", "ecxsys_predicted"))) {
+        if (is.data.frame(model)) {
+            concentration <- model$concentration
+            response <- model[, response_name]
+        } else {
+            stop("Invalid first argument.")
+        }
+    } else if (inherits(model, "ecxsys")) {
+        concentration <- model$curves$concentration
+        response <- model$curves[, response_name]
+    } else if (inherits(model, "ecxsys_predicted")) {
+        concentration <- model$concentration
+        response <- model[, response_name]
+    }
+
+    reference <- response[1]
+    if (reference == 0) {
+        stop("Reference value is zero, calculation of EC not possible.")
     }
-    target_effect <- (1 - target_effect / 100) * control_effect
-    output <- list(effect = target_effect)
-    # Get the index of where the effect changes from above to below
-    # target_effect:
-    below <- which(effect < target_effect)[1]
+    response_level <- (1 - response_level / 100) * reference
+    output <- list(response_value = response_level)
+
+    # Get the index of where the response changes from above to below
+    # response_level:
+    below <- which(response < response_level)[1]
     above <- below - 1
+
     # linear interpolation
-    dist <- (target_effect - effect[below]) / (effect[above] - effect[below])
+    dist <- (response_level - response[below]) / (response[above] - response[below])
     output$concentration <- dist *
         (concentration[above] - concentration[below]) + concentration[below]
     output
diff --git a/R/ecxsys.R b/R/ecxsys.R
index 4273586..5919120 100644
--- a/R/ecxsys.R
+++ b/R/ecxsys.R
@@ -17,106 +17,58 @@
 #' of 0 at ten times the maximum observed concentration.
 #'
 #' The vectors \code{concentration}, \code{effect_tox_observed} and
-#' \code{effect_tox_env_observed} must be of equal length and should be sorted
-#' by increasing concentration.
+#' \code{effect_tox_env_observed} (if provided) must be of equal length and
+#' sorted by increasing concentration.
 #'
 #' @param concentration A vector of concentrations, one of which must be 0 to
 #'   indicate the control. Should be sorted in ascending order, otherwise it
 #'   will be sorted automatically.
+#' @param hormesis_concentration The concentration where the hormesis occurs.
+#'   This is usually the concentration of the highest effect after the control.
 #' @param effect_tox_observed A vector of effect values observed at the given
 #'   concentrations and in absence of environmental stress. Values must be
 #'   between 0 and \code{effect_max}.
 #' @param effect_tox_env_observed Effect values observed in the presence of
-#'   environmental stress. This argument is optional and can be left out to
-#'   model without environmental stress. Values must be between 0 and
-#'   \code{effect_max}.
-#' @param hormesis_concentration The concentration where the hormesis occurs.
-#'   This is usually the concentration of the highest effect after the control.
-#' @param hormesis_index \strong{Deprecated, will be removed soon.} A single
-#'   integer specifying the index of the hormesis concentration in the
-#'   concentration vector. This argument exists for compatibility with older
-#'   versions of this function.
+#'   environmental stress. Must be between 0 and \code{effect_max}.
 #' @param effect_max The maximum value the effect could possibly reach. For
 #'   survival data in percent this should be 100 (the default).
 #' @param p,q The shape parameters of the beta distribution. Default is 3.2.
 #'
-#' @return The result is a list containing many different objects with the most
-#'   important being \code{curves} and \code{fn}. You can use \code{fn()} to
-#'   calculate the curves at your concentrations of choice, see examples.
-#'   \code{curves} is a data frame with the following columns:
-#'   \describe{
-#'     \item{concentration}{Concentrations regularly spaced on a logarithmic
-#'     scale in the given concentration range. The control is approximated by
-#'     the lowest non-control concentration times 1e-7.}
-#'     \item{effect_tox_simple}{The five-parameter log-logistic model of the
-#'     effect derived from the observations under toxicant stress but without
-#'     environmental stress.}
-#'     \item{effect_tox}{Modeled effect resulting from toxicant and system
-#'     stress.}
-#'     \item{effect_tox_sys}{Modeled effect resulting from toxicant and system
-#'     stress.}
-#'     \item{stress_tox}{The toxicant stress.}
-#'     \item{sys_stress_tox}{System stress under toxicant stress conditions
-#'     without environmental stress.}
-#'     \item{stress_tox_sys}{The sum of \code{stress_tox} and
-#'     \code{sys_stress_tox}.}
-#'     \item{effect_tox_env_simple}{The five-parameter log-logistic model of the
-#'     effect derived from the observations under toxicant stress with
-#'     environmental stress.}
-#'     \item{effect_tox_env}{Modeled effect resulting from toxicant and
-#'     environmental stress.}
-#'     \item{effect_tox_env_sys}{Modeled effect resulting from toxicant,
-#'     environmental and system stress.}
-#'     \item{stress_env}{Environmental stress.}
-#'     \item{stress_tox_env}{The sum of toxicant and environmental stress.}
-#'     \item{sys_stress_tox_env}{System stress under toxicant and
-#'     environmental stress conditions.}
-#'     \item{stress_tox_env_sys}{The sum of \code{stress_tox_env} and
-#'     \code{sys_stress_tox_env}.}
-#'     \item{use_for_plotting}{A boolean vector which is used in the plotting
-#'     functions. It controls which parts of the curves are removed for the
-#'     broken concentration axis.}
-#'   }
+#' @return A list (of class ecxsys) containing many different objects with the
+#'   most important being \code{curves}, a data frame containing effect and
+#'   stress values at different concentrations. See \code{\link{predict_ecxsys}}
+#'   for details.
 #'
 #' @examples model <- ecxsys(
 #'     concentration = c(0, 0.03, 0.3, 3, 10),
+#'     hormesis_concentration = 0.3,
 #'     effect_tox_observed = c(85, 76, 94, 35, 0),
-#'     effect_tox_env_observed = c(24, 23, 32, 0, 0),
-#'     hormesis_concentration = 0.3
+#'     effect_tox_env_observed = c(24, 23, 32, 0, 0)
 #' )
 #'
 #' # Use effect_max if for example the effect is given as the number of
 #' # surviving animals and the initial number of animals is 20:
 #' model <- ecxsys(
 #'     concentration = c(0, 0.03, 0.3, 3, 10),
+#'     hormesis_concentration = 0.3,
 #'     effect_tox_observed = c(17, 15.2, 18.8, 7, 0),
 #'     effect_tox_env_observed = c(4.8, 4.6, 6.4, 0, 0),
-#'     hormesis_concentration = 0.3,
 #'     effect_max = 20
 #' )
 #'
-#' # The returned object contains a function which is useful for calculating
-#' # effect and stress values at specific concentrations:
-#' model$fn(c(0, 0.01, 0.1, 1))
-#'
 #' @export
 ecxsys <- function(concentration,
-                   effect_tox_observed,
-                   effect_tox_env_observed,
                    hormesis_concentration,
-                   hormesis_index,
+                   effect_tox_observed,
+                   effect_tox_env_observed = NULL,
                    effect_max = 100,
-                   #stress_tox_at_hormesis = NULL,
                    p = 3.2,
                    q = 3.2) {
     output <- list(args = as.list(environment()))
+    class(output) <- c("ecxsys", class(output))
 
     original_options <- options()
     on.exit(reset_options(original_options))
-    warn_error_original <- list(
-        warn = getOption("warn"),
-        show.error.messages = getOption("show.error.messages")
-    )
 
 
     # input validation ----------------------------------------------------
@@ -129,28 +81,18 @@ ecxsys <- function(concentration,
     if (length(concentration) > length(unique(concentration))) {
         stop("Concentrations must be unique.")
     }
-
-    m_hc <- missing(hormesis_concentration)
-    m_hi <- missing(hormesis_index)
-    if (m_hc && m_hi) {
-        stop("Pleace specify either hormesis_concentration or hormesis_index.")
-    } else if (!m_hi && !m_hc) {
-        stop("Use either hormesis_concentration or hormesis_index but not both.")
-    } else if (!m_hc) {
-        if (!hormesis_concentration %in% concentration) {
-            stop("hormesis_concentration must be one of the values ",
-                 "in concentration.")
-        }
-        hormesis_index = which(hormesis_concentration == concentration)
+    if (length(hormesis_concentration) != 1) {
+        stop("hormesis_concentration must be a single number.")
     }
-
-    if (length(hormesis_index) != 1) {
-        stop("hormesis_index must be a single integer.")
-    } else if (hormesis_index <= 2 || hormesis_index >= length(concentration)) {
-        stop("hormesis_index must be greater than 2 and less than ",
-             "(length(concentration)).")
+    if (!hormesis_concentration %in% concentration) {
+        stop("hormesis_concentration must equal one of the concentration values.")
+    }
+    hormesis_index = which(hormesis_concentration == concentration)
+    if (hormesis_index <= 2 || hormesis_index >= length(concentration)) {
+        stop("hormesis_concentration must be greater than the lowest ",
+             "non-control concentration and less than the highest concentration")
     }
-    if (missing(effect_tox_env_observed)) {
+    if (is.null(effect_tox_env_observed)) {
         with_env <- FALSE
         effect_tox_env_observed <- rep(NA, length(concentration))
     } else {
@@ -176,16 +118,16 @@ ecxsys <- function(concentration,
     conc_shift <- 2  # Powers of ten to shift the control downwards from the
     # second lowest concentration. This is required to approximate 0 because
     # of the logarithmic axis.
+    if (is.unsorted(concentration)) {
+        stop("The values must be sorted by increasing concentration.")
+    }
     if (any(concentration < 0)) {
         stop("Concentrations must be >= 0")
-    } else if (min(concentration) > 0) {
-        stop("No control is given. The first concentration must be 0.")
-    } else {
-        min_conc <- 10 ^ floor(log10(concentration[2]) - conc_shift)
     }
-    if (is.unsorted(concentration)) {
-        stop("The values must be sorted by increasing concentration.")
+    if (min(concentration) > 0) {
+        stop("No control is given. The first concentration must be 0.")
     }
+    min_conc <- 10 ^ floor(log10(concentration[2]) - conc_shift)
 
 
     # scale observed effect -----------------------------------------------
@@ -195,299 +137,128 @@ ecxsys <- function(concentration,
     effect_tox_env_observed <- effect_tox_env_observed / effect_max
 
 
-    # prepare adjusted control concentration ------------------------------
-    conc_adjust_factor <- 10^-5
-    output$conc_adjust_factor <- conc_adjust_factor
-
-
-    # traditional simple model (LL.5) -------------------------------------
-    conc_with_control_shifted <- c(min_conc, concentration[-1])
-    effect_tox_observed_averaged <- moving_weighted_mean(effect_tox_observed)
-    effect_tox_env_observed_averaged <- moving_weighted_mean(effect_tox_env_observed)
-    temp <- approx(
-        log10(conc_with_control_shifted),
-        effect_tox_observed_averaged,
-        n = 10
-    )
-    conc_interpolated <- 10^temp$x
-    effect_tox_observed_interpolated_simple_model <- temp$y
-    effect_tox_mod_simple <- drc::drm(
-        effect_tox_observed_interpolated_simple_model ~ conc_interpolated,
-        fct = drc::LL.5(fixed = c(
-            NA, 0, effect_tox_observed_averaged[1], NA, NA
-        ))
-    )
-    options(warn_error_original)
-    effect_tox_simple <- predict(effect_tox_mod_simple, data.frame(concentration))
-    output$effect_tox_mod_simple <- effect_tox_mod_simple
-    output$effect_tox_simple <- effect_tox_simple * effect_max
+    # traditional log-logistic model (LL.5) -------------------------------
+    LL5_tox <- fit_LL5_model(min_conc, concentration,
+                             effect_tox_observed,
+                             original_options)
+    output$effect_tox_LL5_mod <- LL5_tox$effect_LL5_mod
+    output$effect_tox_LL5 <- LL5_tox$effect_LL5 * effect_max
     if (with_env) {
-        effect_tox_env_observed_interpolated_simple_model <- approx(
-            log10(conc_with_control_shifted),
-            effect_tox_env_observed_averaged,
-            xout = temp$x
-        )$y
-        effect_tox_env_mod_simple <- drc::drm(
-            effect_tox_env_observed_interpolated_simple_model ~ conc_interpolated,
-            fct = drc::LL.5(fixed = c(
-                NA, 0, effect_tox_env_observed_averaged[1], NA, NA
-            ))
-        )
-        options(warn_error_original)
-        effect_tox_env_simple <- predict(
-            effect_tox_env_mod_simple,
-            data.frame(concentration)
-        )
-        output$effect_tox_env_mod_simple <- effect_tox_env_mod_simple
-        output$effect_tox_env_simple <- effect_tox_env_simple * effect_max
+        LL5_tox_env <- fit_LL5_model(min_conc, concentration,
+                                     effect_tox_env_observed,
+                                     original_options)
+        output$effect_tox_env_LL5_mod <- LL5_tox_env$effect_LL5_mod
+        output$effect_tox_env_LL5 <- LL5_tox_env$effect_LL5 * effect_max
     }
 
 
     # interpolation between subhormesis and hormesis ----------------------
     n_new <- 3  # number of new points
-    len <- n_new + 2  # Add 2 because seq() includes the left and right end.
-    subhormesis_index <- hormesis_index - 1
-
-    concentration_interpolated <- 10^seq(
-        log10(concentration[subhormesis_index]),
-        log10(concentration[hormesis_index]),
-        length.out = len
-    )
-    concentration <- append(
-        concentration,
-        concentration_interpolated[-c(1, len)],
-        subhormesis_index
-    )
-
-    effect_tox_observed_interpolated <- seq(
-        effect_tox_observed[subhormesis_index],
-        effect_tox_observed[hormesis_index],
-        length.out = len
-    )
-    effect_tox_observed <- append(
-        effect_tox_observed,
-        effect_tox_observed_interpolated[-c(1, len)],
-        subhormesis_index
-    )
-
+    concentration <- interpolate(concentration, hormesis_index, n_new, TRUE)
+    effect_tox_observed <- interpolate(effect_tox_observed, hormesis_index, n_new)
     if (with_env) {
-        effect_tox_env_observed_interpolated <- seq(
-            effect_tox_env_observed[subhormesis_index],
-            effect_tox_env_observed[hormesis_index],
-            length.out = len
-        )
-        effect_tox_env_observed <- append(
-            effect_tox_env_observed,
-            effect_tox_env_observed_interpolated[-c(1, len)],
-            subhormesis_index
-        )
+        effect_tox_env_observed <- interpolate(effect_tox_env_observed,
+                                               hormesis_index, n_new)
     }
-
     hormesis_index <- hormesis_index + n_new
-
     # In the output return only the values at the original concentrations
     # and exclude those at the interpolated concentrations:
-    exclude <- seq(subhormesis_index + 1, hormesis_index - 1)
-    keep <- !seq_along(concentration) %in% exclude
+    keep <- !seq_along(concentration) %in% seq(hormesis_index - n_new, hormesis_index - 1)
 
 
     # effect_tox ----------------------------------------------------------
-    effect_tox <- effect_tox_observed
-    effect_tox[1] <- 1
-    effect_to_fit_idx <- 2:(hormesis_index - 1)
-    effect_tox[effect_to_fit_idx] <- NA
-    effect_tox_mod <- drc::drm(
-        effect_tox ~ concentration,
-        fct = drc::W1.2()
-    )
-    options(warn_error_original)
-    effect_tox <- predict(
-        effect_tox_mod,
-        data.frame(concentration = concentration)
-    )
+    effect_tox <- c(1, effect_tox_observed[-1])
+    effect_tox[2:(hormesis_index - 1)] <- NA
+    effect_tox_mod <- drc::drm(effect_tox ~ concentration, fct = drc::W1.2())
+    options(original_options["warn"])
+    effect_tox <- predict(effect_tox_mod, data.frame(concentration = concentration))
     output$effect_tox_mod <- effect_tox_mod
     output$effect_tox <- effect_tox[keep] * effect_max
 
-
-    # system stress without environmental stress --------------------------
-    stress_tox_observed <- effect_to_stress(
-        effect_tox_observed, p, q
-    )
     stress_tox <- effect_to_stress(effect_tox, p, q)
     output$stress_tox <- stress_tox[keep]
-    sys_stress_tox <- stress_tox_observed - stress_tox
-    sys_stress_tox <- pmin(pmax(sys_stress_tox, 0), 1)
-    sys_stress_tox[hormesis_index:length(sys_stress_tox)] <- 0
-    # Add sys_stress_tox to the output before it is fitted:
-    output$sys_stress_tox <- sys_stress_tox[keep]
-    sys_stress_tox_mod <- tryCatch(
-        {
-            # There is no other way to suppress that one error message
-            # except by changing the options temporarily.
-            options(show.error.messages = FALSE)
-            drc::drm(sys_stress_tox ~ stress_tox, fct = drc::W1.3())
-        },
-        error = function(e) {
-            options(warn_error_original)
-            warning(
-                "Using a horizontal linear model for sys_stress_tox_mod ",
-                "because the Weibull model did not converge.",
-                call. = FALSE
-            )
-            # Failure to converge often happens when all or almost all sys
-            # stress values are zero. Fitting a linear model in this case seems
-            # to be the most appropriate remedy.
-            stress_tox <- range(stress_tox)
-            sys_stress_tox <- c(0, 0)
-            return(lm(sys_stress_tox ~ stress_tox))
-        },
-        finally = options(warn_error_original)
-    )
-    output$sys_stress_tox_mod <- sys_stress_tox_mod
-    sys_stress_tox <- unname(
-        predict(sys_stress_tox_mod, data.frame(stress_tox))
+
+
+    # system stress without environmental stress --------------------------
+    fit_sys_output <- fit_sys(
+        effect_to_stress(effect_tox_observed, p, q),
+        stress_tox,
+        stress_tox,
+        hormesis_index,
+        original_options
     )
+    output$sys_tox_not_fitted <- fit_sys_output$sys[keep]
+    output$sys_tox_mod <- fit_sys_output$sys_mod
+    if (inherits(fit_sys_output$sys_mod, "lm")) {
+        warning(
+            "Using a horizontal linear model for sys_tox_mod ",
+            "because the Weibull model did not converge."
+        )
+    }
+    sys_tox <- fit_sys_output$sys_modeled
+    output$sys_tox <- sys_tox
 
 
     # modeled effect without environmental stress -------------------------
-    stress_tox_sys <- stress_tox + sys_stress_tox
+    stress_tox_sys <- stress_tox + sys_tox
     effect_tox_sys <- stress_to_effect(stress_tox_sys, p, q)
     output$effect_tox_sys <- effect_tox_sys[keep] * effect_max
 
 
     if (with_env) {
         # env stress ------------------------------------------------------
-        stress_tox_env_observed <- effect_to_stress(
-            effect_tox_env_observed, p, q
-        )
+        stress_tox_env_observed <- effect_to_stress(effect_tox_env_observed, p, q)
         stress_env <- (stress_tox_env_observed - stress_tox)[hormesis_index]
-        stress_env <- pmax(stress_env, 0)
+        stress_env <- clamp(stress_env)
         output$stress_env <- stress_env
 
-
-        # system stress with environmental stress -------------------------
         stress_tox_env <- stress_tox + stress_env
-        effect_tox_env <- stress_to_effect(stress_tox_env, p, q)
         output$stress_tox_env <- stress_tox_env[keep]
+        effect_tox_env <- stress_to_effect(stress_tox_env, p, q)
         output$effect_tox_env <- effect_tox_env[keep] * effect_max
-        sys_stress_tox_env <- stress_tox_env_observed - stress_tox_env
-        sys_stress_tox_env <- pmin(pmax(sys_stress_tox_env, 0), 1)
-        sys_stress_tox_env[hormesis_index:length(sys_stress_tox_env)] <- 0
-        # Add sys_stress_tox_env to the output before it is fitted:
-        output$sys_stress_tox_env <- sys_stress_tox_env[keep]
-        sys_stress_tox_env_mod <- tryCatch(
-            {
-                # There is no other way to suppress that one error message
-                # except by changing the options temporarily.
-                options(show.error.messages = FALSE)
-                drc::drm(sys_stress_tox_env ~ stress_tox, fct = drc::W1.3())
-            },
-            error = function(e) {
-                options(warn_error_original)
-                warning(
-                    "Using a horizontal linear model for ",
-                    "sys_stress_tox_env_mod because the Weibull model did ",
-                    "not converge.",
-                    call. = FALSE
-                )
-                # Failure to converge often happens when all or almost all sys
-                # stress values are zero. Fitting a linear model in this case
-                # seems to be the most appropriate remedy.
-                stress_tox <- range(stress_tox)
-                sys_stress_tox_env <- c(0, 0)
-                return(lm(sys_stress_tox_env ~ stress_tox))
-            },
-            finally = options(warn_error_original)
-        )
-        output$sys_stress_tox_env_mod <- sys_stress_tox_env_mod
-        sys_stress_tox_env <- unname(
-            predict(sys_stress_tox_env_mod, data.frame(stress_tox))
+
+
+        # system stress with environmental stress -------------------------
+        fit_sys_output <- fit_sys(
+            stress_tox_env_observed,
+            stress_tox_env,
+            stress_tox,
+            hormesis_index,
+            original_options
         )
+        output$sys_tox_env_not_fitted <- fit_sys_output$sys[keep]
+        output$sys_tox_env_mod <- fit_sys_output$sys_mod
+        if (inherits(fit_sys_output$sys_mod, "lm")) {
+            warning(
+                "Using a horizontal linear model for sys_tox_env_mod ",
+                "because the Weibull model did not converge."
+            )
+        }
+        sys_tox_env <- fit_sys_output$sys_modeled
+        output$sys_tox_env <- sys_tox_env
 
 
         # modeled effect with environmental stress ------------------------
-        stress_tox_env_sys <- stress_tox_env + sys_stress_tox_env
+        stress_tox_env_sys <- stress_tox_env + sys_tox_env
         effect_tox_env_sys <- stress_to_effect(stress_tox_env_sys, p, q)
         output$effect_tox_env_sys <- effect_tox_env_sys[keep] * effect_max
     }
 
 
-    # building the function -----------------------------------------------
-    fn <- function(conc) {
-        # This function returns all modeled values at the provided
-        # concentrations. conc = a vector of concentrations
-        stopifnot(is.numeric(conc))
-        effect_tox_simple_fn <- predict(
-            effect_tox_mod_simple,
-            data.frame(concentration = conc)
-        )
-        effect_tox_fn <- predict(
-            effect_tox_mod,
-            data.frame(concentration = conc)
-        )
-        stress_tox_fn <- effect_to_stress(effect_tox_fn, p, q)
-        sys_stress_tox_fn <- predict(
-            sys_stress_tox_mod,
-            data.frame(stress_tox = stress_tox_fn)
-        )
-        stress_tox_sys_fn <- stress_tox_fn + sys_stress_tox_fn
-        effect_tox_sys_fn <- stress_to_effect(stress_tox_sys_fn, p, q)
-
-        out_df <- data.frame(
-            concentration = conc,
-            effect_tox_simple = effect_tox_simple_fn * effect_max,
-            effect_tox = effect_tox_fn * effect_max,
-            effect_tox_sys = effect_tox_sys_fn * effect_max,
-            stress_tox = stress_tox_fn,
-            sys_stress_tox = sys_stress_tox_fn,
-            stress_tox_sys = stress_tox_sys_fn
-        )
-        if (with_env) {
-            effect_tox_env_simple_fn = predict(
-                effect_tox_env_mod_simple,
-                data.frame(concentration = conc)
-            )
-            stress_tox_env_fn <- stress_tox_fn + stress_env
-            effect_tox_env_fn <- stress_to_effect(
-                stress_tox_env_fn, p, q
-            )
-            sys_stress_tox_env_fn <- predict(
-                sys_stress_tox_env_mod,
-                data.frame(stress_tox = stress_tox_fn)
-            )
-            stress_tox_env_sys_fn <- stress_tox_env_fn +
-                sys_stress_tox_env_fn
-            effect_tox_env_sys_fn <- stress_to_effect(
-                stress_tox_env_sys_fn, p, q
-            )
-            out_df <- cbind(out_df, data.frame(
-                effect_tox_env_simple = effect_tox_env_simple_fn * effect_max,
-                effect_tox_env = effect_tox_env_fn * effect_max,
-                effect_tox_env_sys = effect_tox_env_sys_fn * effect_max,
-                stress_env = stress_env,
-                stress_tox_env = stress_tox_env_fn,
-                sys_stress_tox_env = sys_stress_tox_env_fn,
-                stress_tox_env_sys = stress_tox_env_sys_fn
-            ))
-        }
-        out_df
-    }
-
-    output$fn <- fn
-
-
     # smooth curves -------------------------------------------------------
     # In order to generate a broken x-axis the concentration vector must
     # also be broken in two. The left part of the axis is supposed to be at
     # 0 but because it's a log axis I have to make the values just really
     # small. The concentrations in the gap won't be used for plotting later.
     n_smooth <- 1000  #  number of points to approximate the curves
+    conc_adjust_factor <- 10^-5
+    output$conc_adjust_factor <- conc_adjust_factor
     concentration_smooth <- 10 ^ seq(
         log10(min_conc * conc_adjust_factor),
         log10(max(concentration)),
         length.out = n_smooth
     )
-    output$curves <- fn(concentration_smooth)
+    output$curves <- predict_ecxsys(output, concentration_smooth)
     output$curves$use_for_plotting <-
         concentration_smooth < min_conc * conc_adjust_factor * 1.5 |
         concentration_smooth > min_conc * 1.5
@@ -513,3 +284,99 @@ reset_options <- function(original_options) {
     }
     options(changed)
 }
+
+
+fit_sys <- function(stress_external_observed,
+                    stress_external_modeled,
+                    stress_tox,
+                    hormesis_index,
+                    original_options) {
+    sys <- stress_external_observed - stress_external_modeled
+    sys <- clamp(sys)
+    sys[hormesis_index:length(sys)] <- 0
+    # Add sys to the output before it is fitted:
+    out <- list(sys = sys)
+    sys_mod <- tryCatch(
+        {
+            # There is no other way to suppress that one error message
+            # from optim except by changing the options temporarily.
+            warn_error_original <- original_options[c("warn", "show.error.messages")]
+            options(show.error.messages = FALSE)
+            drc::drm(sys ~ stress_tox, fct = drc::W1.3())
+        },
+        error = function(e) {
+            # Failure to converge often happens when all or almost all sys
+            # stress values are zero. Fitting a linear model in this case seems
+            # to be the most appropriate remedy.
+            stress_tox <- range(stress_tox)
+            sys <- c(0, 0)
+            lm(sys ~ stress_tox)
+        },
+        finally = options(warn_error_original)
+    )
+    out$sys_mod <- sys_mod
+    out$sys_modeled <- unname(
+        predict(sys_mod, data.frame(stress_tox = stress_tox))
+    )
+    out
+}
+
+
+moving_weighted_mean <- function(x) {
+    # This is used to smooth out points which are jumping up and down.
+    count <- rep(1, length(x))
+    x_diff <- diff(x) * -1
+    while (any(x_diff < 0, na.rm = TRUE)) {
+        i <- which(x_diff < 0)[1]
+        j <- i + 1
+        x[i] <- weighted.mean(x[i:j], count[i:j])
+        x <- x[-j]
+        count[i] <- count[i] + count[j]
+        count <- count[-j]
+        x_diff <- diff(x) * -1
+    }
+    rep(x, count)
+}
+
+
+fit_LL5_model <- function(min_conc,
+                          concentration,
+                          effect_observed,
+                          original_options) {
+    # The traditional log-logistic model, here using the five-parameter
+    # log-logistic function drc::LL.5.
+    conc_with_control_shifted <- c(min_conc, concentration[-1])
+    effect_observed_averaged <- moving_weighted_mean(effect_observed)
+    interpolated <- approx(
+        log10(conc_with_control_shifted),
+        effect_observed_averaged,
+        n = 10
+    )
+    conc_interpolated <- 10^interpolated$x
+    effect_interpolated <- interpolated$y
+    effect_LL5_mod <- drc::drm(
+        effect_interpolated ~ conc_interpolated,
+        fct = drc::LL.5(fixed = c(NA, 0, effect_observed_averaged[1], NA, NA))
+    )
+    options(original_options["warn"])
+    effect_LL5 <- predict(
+        effect_LL5_mod,
+        data.frame(conc_interpolated = concentration)
+    )
+    list(
+        effect_LL5_mod = effect_LL5_mod,
+        effect_LL5 = effect_LL5
+    )
+}
+
+
+interpolate <- function(x, to_index, n_new, conc = FALSE) {
+    from_index <- to_index - 1  # subhormesis_index
+    len <- n_new + 2  # Add 2 because seq() includes the left and right end.
+    if (conc) {
+        x_new <- 10^seq(log10(x[from_index]), log10(x[to_index]), length.out = len)
+    } else {
+        x_new <- seq(x[from_index], x[to_index], length.out = len)
+    }
+    append(x, x_new[-c(1, len)], from_index)
+}
diff --git a/R/helpers.R b/R/helpers.R
new file mode 100644
index 0000000..f13f0d0
--- /dev/null
+++ b/R/helpers.R
@@ -0,0 +1,59 @@
+# A collection of internal helper functions which get used in more than one place.
+
+
+clamp <- function(x, lower = 0, upper = 1) {
+    # Returns lower if x < lower, returns upper if x > upper, else returns x
+    pmin(pmax(x, lower), upper)
+}
+
+
+get_log_ticks <- function(x) {
+    # Calculate the positions of major and minor tick marks on a base 10
+    # logarithmic axis.
+    stopifnot(min(x, na.rm = TRUE) > 0)
+    x <- log10(x)
+    major <- 10 ^ seq(
+        floor(min(x, na.rm = TRUE)),
+        ceiling(max(x, na.rm = TRUE))
+    )
+    n_between <- length(major) - 1
+    minor <- integer(n_between * 8)
+    for (i in 1:n_between) {
+        a <- major[i]
+        b <- major[i + 1]
+        minor[seq(i * 8 - 7, i * 8)] <- seq(a + a, b - a, a)
+    }
+    major_tick_labels <- formatC(major, format = "fg")
+    major_tick_labels[1] <- "0"
+    list(
+        major = major,
+        minor = minor,
+        major_labels = major_tick_labels
+    )
+}
+
+
+adjust_smooth_concentrations <- function(curves, conc_adjust_factor) {
+    # Helper for the plot functions, not exported.
+    # Deals with the concentrations which are unnecessary for plotting.
+    # This means it removes the concentrations in the gap and scales the
+    # concentrations left of the gap up.
+
+    gap_idx <- min(which(!curves$use_for_plotting))
+
+    # Keep only the values to the left and right of the axis break:
+    curves <- curves[curves$use_for_plotting, ]
+
+    # Add NAs to force breaks in the lines:
+    axis_break_conc <- curves$concentration[gap_idx]
+    curves[gap_idx, ] <- NA
+
+    # Shift the small concentrations upwards so the plot has a nice x-axis:
+    curves$concentration[1:gap_idx] <-
+        curves$concentration[1:gap_idx] / conc_adjust_factor
+
+    list(
+        curves = curves,
+        axis_break_conc = axis_break_conc
+    )
+}
diff --git a/R/moving_weighted_mean.R b/R/moving_weighted_mean.R
deleted file mode 100644
index a9e1e8e..0000000
--- a/R/moving_weighted_mean.R
+++ /dev/null
@@ -1,16 +0,0 @@
-moving_weighted_mean <- function(x) {
-    # Helper function to calculate the moving weighted mean. This is used to
-    # smooth out points which are jumping up and down.
-    count <- rep(1, length(x))
-    x_diff <- diff(x) * -1
-    while (any(x_diff < 0, na.rm = TRUE)) {
-        i <- which(x_diff < 0)[1]
-        j <- i + 1
-        x[i] <- weighted.mean(x[i:j], count[i:j])
-        x <- x[-j]
-        count[i] <- count[i] + count[j]
-        count <- count[-j]
-        x_diff <- diff(x) * -1
-    }
-    rep(x, count)
-}
diff --git a/R/plot_ecxsys.R b/R/plot_ecxsys.R
index cf3f28e..0ea5e49 100644
--- a/R/plot_ecxsys.R
+++ b/R/plot_ecxsys.R
@@ -8,74 +8,19 @@
 #'
 #' @name plot_ecxsys
 #'
-#' @param model The list returned from ecxsys().
-#' @param show_simple_model Should the log-logistic models be plotted for
+#' @param model The list returned from \code{\link{ecxsys}}.
+#' @param show_LL5_model Should the log-logistic models be plotted for
 #'   comparison? Defaults to \code{FALSE}.
 #' @param show_legend Should the plot include a legend? Defaults to FALSE
 #'   because it may cover some points or lines depending on the plot size.
 #' @param xlab,ylab Axis labels.
 #'
 #' @examples model <- ecxsys(
-#'     effect_tox_observed = c(85, 76, 94, 35, 0),
-#'     effect_tox_env_observed = c(24, 23, 32, 0, 0),
 #'     concentration = c(0, 0.03, 0.3, 3, 10),
-#'     hormesis_concentration = 0.3
+#'     hormesis_concentration = 0.3,
+#'     effect_tox_observed = c(85, 76, 94, 35, 0),
+#'     effect_tox_env_observed = c(24, 23, 32, 0, 0)
 #' )
 #' plot_effect(model)
-#' plot_system_stress(model)
+#' plot_stress(model)
 NULL
-
-
-
-# internal helper functions for plotting ----------------------------------
-
-get_log_ticks <- function(x) {
-    # Calculate the positions of major and minor tick marks on a base 10
-    # logarithmic axis.
-    stopifnot(min(x, na.rm = TRUE) > 0)
-    x <- log10(x)
-    major <- 10 ^ seq(
-        floor(min(x, na.rm = TRUE)),
-        ceiling(max(x, na.rm = TRUE))
-    )
-    n_between <- length(major) - 1
-    minor <- integer(n_between * 8)
-    for (i in 1:n_between) {
-        a <- major[i]
-        b <- major[i+1]
-        minor[seq(i * 8 - 7, i * 8)] <- seq(a + a, b - a, a)
-    }
-    major_tick_labels <- formatC(major, format = "fg")
-    major_tick_labels[1] <- "0"
-    list(
-        major = major,
-        minor = minor,
-        major_labels = major_tick_labels
-    )
-}
-
-
-adjust_smooth_concentrations <- function(curves, conc_adjust_factor) {
-    # Helper for the plot functions, not exported.
-    # Deals with the concentrations which are unnecessary for plotting.
-    # This means it removes the concentrations in the gap and scales the
-    # concentrations left of the gap up.
-
-    gap_idx <- min(which(!curves$use_for_plotting))
-
-    # Keep only the values to the left and right of the axis break:
-    curves <- curves[curves$use_for_plotting, ]
-
-    # Add NAs to force breaks in the lines:
-    axis_break_conc <- curves$concentration[gap_idx]
-    curves[gap_idx, ] <- NA
-
-    # Shift the small concentrations upwards so the plot has a nice x-axis:
-    curves$concentration[1:gap_idx] <-
-        curves$concentration[1:gap_idx] / conc_adjust_factor
-
-    list(
-        curves = curves,
-        axis_break_conc = axis_break_conc
-    )
-}
diff --git a/R/plot_effect.R b/R/plot_effect.R
index cc4995c..6b944e3 100644
--- a/R/plot_effect.R
+++ b/R/plot_effect.R
@@ -1,17 +1,18 @@
 #' @rdname plot_ecxsys
 #' @export
 plot_effect <- function(model,
-                        show_simple_model = FALSE,
+                        show_LL5_model = FALSE,
                         show_legend = FALSE,
                         xlab = "concentration",
                         ylab = "effect") {
+    stopifnot(inherits(model, "ecxsys"))
     temp <- adjust_smooth_concentrations(
         model$curves,
         model$conc_adjust_factor
     )
     curves <- temp$curves
     log_ticks <- get_log_ticks(curves$concentration)
-    model$args$concentration[1] <- curves$concentration[1]
+    concentration <- c(curves$concentration[1], model$args$concentration[-1])
 
     plot(
         NA,
@@ -25,85 +26,82 @@ plot_effect <- function(model,
         bty = "L",
         las = 1
     )
-    if (show_simple_model) {
-        lines(
-            curves$concentration,
-            curves$effect_tox_simple,
-            col = rgb(112, 112, 207, maxColorValue = 255),
-            lty = 4
-        )
-        if (model$with_env) {
-            lines(
-                curves$concentration,
-                curves$effect_tox_env_simple,
-                col = rgb(207, 112, 112, maxColorValue = 255),
-                lty = 4
-            )
-        }
-    }
 
     lines(
         curves$concentration,
         curves$effect_tox_sys,
         col = "blue"
     )
-    if (model$with_env) {
-        lines(
-            curves$concentration,
-            curves$effect_tox_env_sys,
-            col = "red"
-        )
-    }
     lines(
         curves$concentration,
         curves$effect_tox,
         col = "deepskyblue",
         lty = 2
     )
+    points(
+        concentration,
+        model$args$effect_tox_observed,
+        pch = 16,
+        col = "blue"
+    )
+
     if (model$with_env) {
+        lines(
+            curves$concentration,
+            curves$effect_tox_env_sys,
+            col = "red"
+        )
         lines(
             curves$concentration,
             curves$effect_tox_env,
             col = "orange",
             lty = 2
         )
-    }
-    points(
-        model$args$concentration,
-        model$args$effect_tox_observed,
-        pch = 16,
-        col = "blue"
-    )
-    if (model$with_env) {
-        # TODO: merge this with the same condition above.
         points(
-            model$args$concentration,
+            concentration,
             model$args$effect_tox_env_observed,
             pch = 16,
             col = "red"
         )
     }
+
+    if (show_LL5_model) {
+        lines(
+            curves$concentration,
+            curves$effect_tox_LL5,
+            col = "darkblue",
+            lty = 4
+        )
+        if (model$with_env) {
+            lines(
+                curves$concentration,
+                curves$effect_tox_env_LL5,
+                col = "darkred",
+                lty = 4
+            )
+        }
+    }
+
     axis(1, at = log_ticks$major, labels = log_ticks$major_labels)
     axis(1, at = log_ticks$minor, labels = FALSE, tcl = -0.25)
     plotrix::axis.break(1, breakpos = temp$axis_break_conc)
+
     if (show_legend) {
-        # TODO: Maybe do proper subscript in legend
-        # TODO: Add simple models to the legend
-        legend_text <- c(
-            "effect (tox+sys)",
-            "effect (tox+env+sys)",
-            "toxicant effect",
-            "tox_env effect"
-        )
-        legend_pch <- c(16, 16, NA, NA)
-        legend_lty <- c(1, 1, 2, 2)
-        legend_col <- c("blue", "red", "deepskyblue", "orange")
-        if (!model$with_env) {
-            keep <- c(1, 3)
-            legend_text <- legend_text[keep]
-            legend_pch <- legend_pch[keep]
-            legend_lty <- legend_lty[keep]
-            legend_col <- legend_col[keep]
+        legend_text <- c("tox", "tox + sys")
+        legend_pch <- c(NA, 16)
+        legend_lty <- c(2, 1)
+        legend_col <- c("deepskyblue", "blue")
+        if (model$with_env) {
+            legend_text <- c(legend_text, "tox + env", "tox + env + sys")
+            legend_pch <- c(legend_pch, NA, 16)
+            legend_lty <- c(legend_lty, 2, 1)
+            legend_col <- c(legend_col, "orange", "red")
+        }
+        if (show_LL5_model) {
+            legend_text <- c(legend_text, "tox (LL5)", "tox + env (LL5)")
+            legend_pch <- c(legend_pch, NA, NA)
+            legend_lty <- c(legend_lty, 4, 4)
+            legend_col <- c(legend_col, "darkblue", "darkred")
         }
         legend(
             "topright",
diff --git a/R/plot_system_stress.R b/R/plot_stress.R
similarity index 62%
rename from R/plot_system_stress.R
rename to R/plot_stress.R
index 72c7326..cccdcbc 100644
--- a/R/plot_system_stress.R
+++ b/R/plot_stress.R
@@ -1,6 +1,7 @@
 #' @rdname plot_ecxsys
 #' @export
-plot_system_stress <- function(model, show_legend = FALSE) {
+plot_stress <- function(model, show_legend = FALSE) {
+    stopifnot(inherits(model, "ecxsys"))
     temp <- adjust_smooth_concentrations(
         model$curves,
         model$conc_adjust_factor
@@ -8,8 +9,7 @@ plot_system_stress <- function(model, show_legend = FALSE) {
     curves <- temp$curves
     axis_break_conc <- temp$axis_break_conc
     log_ticks <- get_log_ticks(curves$concentration)
-    # FIXME: Don't overwrite this arg. Make a new variable instead.
-    model$args$concentration[1] <- curves$concentration[1]
+    concentration <- c(curves$concentration[1], model$args$concentration[-1])
 
     plot(
         NA,
@@ -23,43 +23,43 @@ plot_system_stress <- function(model, show_legend = FALSE) {
         las = 1,
         bty = "L"
     )
+
     lines(
         curves$concentration,
-        curves$sys_stress_tox,
+        curves$sys_tox,
         col = "blue"
     )
     points(
-        model$args$concentration,
-        model$sys_stress_tox,
+        concentration,
+        model$sys_tox_not_fitted,
         pch = 16,
         col = "blue"
     )
+
     if (model$with_env) {
         lines(
             curves$concentration,
-            curves$sys_stress_tox_env,
+            curves$sys_tox_env,
             col = "red"
         )
         points(
-            model$args$concentration,
-            model$sys_stress_tox_env,
+            concentration,
+            model$sys_tox_env_not_fitted,
             pch = 16,
             col = "red"
         )
     }
+
     axis(1, at = log_ticks$major, labels = log_ticks$major_labels)
     axis(1, at = log_ticks$minor, labels = FALSE, tcl = -0.25)
     plotrix::axis.break(1, breakpos = axis_break_conc)
+
     if (show_legend) {
-        legend_text <- c(
-            # TODO: Maybe do proper subscript in legend
-            "system stress (tox)",
-            "system stress (tox+env)"
-        )
-        legend_col <- c("blue", "red")
-        if (!model$with_env) {
-            legend_text <- legend_text[1]
-            legend_col <- legend_col[1]
+        legend_text <- c("tox")
+        legend_col <- c("blue")
+        if (model$with_env) {
+            legend_text <- c(legend_text, "tox + env")
+            legend_col <- c(legend_col, "red")
         }
         legend(
             "topright",
diff --git a/R/predict_ecxsys.R b/R/predict_ecxsys.R
new file mode 100644
index 0000000..604dce4
--- /dev/null
+++ b/R/predict_ecxsys.R
@@ -0,0 +1,122 @@
+#' Predict ECxSyS at various concentrations
+#'
+#' @param model The output of a call to \code{\link{ecxsys}}.
+#' @param concentration A numeric vector of concentrations.
+#'
+#' @return A data frame (of class "ecxsys_predicted") with the following
+#'   columns:
+#'   \describe{
+#'     \item{concentration}{Concentrations regularly spaced on a logarithmic
+#'     scale in the given concentration range. The control is approximated by
+#'     the lowest non-control concentration times 1e-7.}
+#'     \item{effect_tox_LL5}{The five-parameter log-logistic model of the
+#'     effect derived from the observations under toxicant stress but without
+#'     environmental stress.}
+#'     \item{effect_tox}{Modeled effect resulting from toxicant and system
+#'     stress.}
+#'     \item{effect_tox_sys}{Modeled effect resulting from toxicant and system
+#'     stress.}
+#'     \item{stress_tox}{The toxicant stress.}
+#'     \item{sys_tox}{System stress under toxicant stress conditions
+#'     without environmental stress.}
+#'     \item{stress_tox_sys}{The sum of \code{stress_tox} and
+#'     \code{sys_tox}.}
+#'     \item{effect_tox_env_LL5}{The five-parameter log-logistic model of the
+#'     effect derived from the observations under toxicant stress with
+#'     environmental stress.}
+#'     \item{effect_tox_env}{Modeled effect resulting from toxicant and
+#'     environmental stress.}
+#'     \item{effect_tox_env_sys}{Modeled effect resulting from toxicant,
+#'     environmental and system stress.}
+#'     \item{stress_env}{Environmental stress.}
+#'     \item{stress_tox_env}{The sum of toxicant and environmental stress.}
+#'     \item{sys_tox_env}{System stress under toxicant and
+#'     environmental stress conditions.}
+#'     \item{stress_tox_env_sys}{The sum of \code{stress_tox_env} and
+#'     \code{sys_tox_env}.}
+#'   }
+#'
+#' @examples model <- ecxsys(
+#'     concentration = c(0, 0.03, 0.3, 3, 10),
+#'     hormesis_concentration = 0.3,
+#'     effect_tox_observed = c(85, 76, 94, 35, 0)
+#' )
+#' p <- predict_ecxsys(model, c(0.001, 0.01, 0.1, 1, 10))
+#'
+#' @export
+predict_ecxsys <- function(model, concentration) {
+    # This function returns all modeled values at the provided
+    # concentrations.
+    stopifnot(
+        inherits(model, "ecxsys"),
+        is.numeric(concentration)
+    )
+    p <- model$args$p
+    q <- model$args$q
+    effect_max <- model$args$effect_max
+    out_df <- data.frame(
+        concentration = concentration
+    )
+
+    out_df$effect_tox_LL5 <- predict(
+        model$effect_tox_LL5_mod,
+        data.frame(concentration = concentration)
+    ) * effect_max
+
+    if (model$with_env) {
+        out_df$effect_tox_env_LL5 <- predict(
+            model$effect_tox_env_LL5_mod,
+            data.frame(concentration = concentration)
+        ) * effect_max
+    }
+
+    effect_tox <- predict(
+        model$effect_tox_mod,
+        data.frame(concentration = concentration)
+    )
+    out_df$effect_tox <- effect_tox * effect_max
+
+    stress_tox <- effect_to_stress(effect_tox, p, q)
+    out_df$stress_tox <- stress_tox
+
+    sys_tox <- predict(
+        model$sys_tox_mod,
+        data.frame(stress_tox = stress_tox)
+    )
+    out_df$sys_tox <- sys_tox
+
+    stress_tox_sys <- stress_tox + sys_tox
+    out_df$stress_tox_sys <- stress_tox_sys
+
+    effect_tox_sys <- stress_to_effect(stress_tox_sys, p, q)
+    out_df$effect_tox_sys <- effect_tox_sys * effect_max
+
+    if (model$with_env) {
+        out_df$stress_env <- model$stress_env
+
+        stress_tox_env <- stress_tox + model$stress_env
+        out_df$stress_tox_env <- stress_tox_env
+
+        out_df$effect_tox_env <- stress_to_effect(
+            stress_tox_env, p, q
+        ) * effect_max
+
+        sys_tox_env <- predict(
+            model$sys_tox_env_mod,
+            data.frame(stress_tox = stress_tox)
+        )
+        out_df$sys_tox_env <- sys_tox_env
+
+        stress_tox_env_sys <- stress_tox_env +
+            sys_tox_env
+        out_df$stress_tox_env_sys <- stress_tox_env_sys
+
+        effect_tox_env_sys <- stress_to_effect(
+            stress_tox_env_sys, p, q
+        )
+        out_df$effect_tox_env_sys <- effect_tox_env_sys * effect_max
+    }
+
+    class(out_df) <- c("ecxsys_predicted", class(out_df))
+    out_df
+}
diff --git a/R/stress_effect_conversion.R b/R/stress_effect_conversion.R
index 512f86a..ef6c252 100644
--- a/R/stress_effect_conversion.R
+++ b/R/stress_effect_conversion.R
@@ -6,7 +6,7 @@
 #' These are simple wrappers around the beta distribution function
 #' \code{\link[stats:Beta]{pbeta}} and the beta quantile function
 #' \code{\link[stats:Beta]{qbeta}}. \code{stress_to_effect} returns
-#' \code{1 - pbeta(stress, p, q)}. \code{effect_to_stress} first clips the
+#' \code{1 - pbeta(stress, p, q)}. \code{effect_to_stress} first clamps the
 #' effect to the interval [0, 1] and then returns
 #' \code{qbeta(1 - effect, p, q)}.
 #'
@@ -15,11 +15,12 @@
 #'
 #' @name Stressconversion
 #'
-#' @param effect One or more effect values to convert to general stress.
-#'   Should be a value between 0 and 1. Smaller or bigger values are treated as
-#'   0 or 1 respectively.
+#' @param effect One or more effect values to convert to general stress. Should
+#'   be a value between 0 and 1. Smaller or bigger values are treated as 0 or 1
+#'   respectively.
 #' @param stress One or more stress values to convert to effect.
-#' @param p,q The shape parameters of the beta distribution. Default is 3.2.
+#' @param p,q The shape parameters of the \code{\link[stats:Beta]{beta}}
+#'   distribution. Default is 3.2.
 #'
 #' @examples
 #' stress <- 0.3
@@ -32,7 +33,8 @@ NULL
 #' @rdname Stressconversion
 #' @export
 effect_to_stress <- function(effect, p = 3.2, q = 3.2) {
-    effect <- pmin(pmax(effect, 0), 1)
+    stopifnot(p >= 0, q >= 0)
+    effect <- clamp(effect)
     qbeta(1 - effect, p, q)
 }
 
@@ -40,5 +42,6 @@ effect_to_stress <- function(effect, p = 3.2, q = 3.2) {
 #' @rdname Stressconversion
 #' @export
 stress_to_effect <- function(stress, p = 3.2, q = 3.2) {
+    stopifnot(p >= 0, q >= 0)
     1 - pbeta(stress, p, q)
 }
diff --git a/R/stressaddition-package.R b/R/stressaddition-package.R
index 649790b..16dd5a0 100644
--- a/R/stressaddition-package.R
+++ b/R/stressaddition-package.R
@@ -2,6 +2,10 @@
 # ?`stressaddition-package`. Some people recommend using "@keywords internal"
 # here to exclude this page from the help index but I'm not going to do that.
 
+# "_PACKAGE" automatically populates most sections in this package
+# documentation, so I don't need to manually fill in authors, title or
+# description.
+
 
 #' @details See the publication linked below for more information including
 #'   equations.
@@ -23,8 +27,3 @@
 #' @import graphics
 #' @import grDevices
 "_PACKAGE"
-
-
-# "_PACKAGE" automatically populates most sections in this package
-# documentation, so I don't need to manually fill in authors, title or
-# description.
diff --git a/README.md b/README.md
index 7bc2506..d52dcf3 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,7 @@ Alternatively there are binary and source builds downloadable from the [releases
 ## Updating
 RStudio's integrated package updater won't detect updates in packages installed from GitHub or GitLab. I recommend running 
 ```r
-# install.packages("remotes")
-remotes::update_packages()
+devtools::update_packages()
 ```
 in regular intervals to check for updates from those sources.
 
@@ -28,15 +27,15 @@ In the paper we use the model in the context of survival experiments. However, i
 library(stressaddition)
 model <- ecxsys(
     concentration = c(0, 0.03, 0.3, 3, 10),
+    hormesis_concentration = 0.3,
     effect_tox_observed = c(85, 76, 94, 35, 0),
-    effect_tox_env_observed = c(24, 23, 32, 0, 0),
-    hormesis_concentration = 0.3
+    effect_tox_env_observed = c(24, 23, 32, 0, 0)
 )
 
 # Plot the effect and the system stress:
 par(mfrow = c(2, 1))
 plot_effect(model)
-plot_system_stress(model)
+plot_stress(model)
 
 # The LC50 of the effect under the influence of toxicant and system tress:
 ec(model, "effect_tox_sys", 50)
diff --git a/man/Stressconversion.Rd b/man/Stressconversion.Rd
index b84b0f9..2762fee 100644
--- a/man/Stressconversion.Rd
+++ b/man/Stressconversion.Rd
@@ -11,11 +11,12 @@ effect_to_stress(effect, p = 3.2, q = 3.2)
 stress_to_effect(stress, p = 3.2, q = 3.2)
 }
 \arguments{
-\item{effect}{One or more effect values to convert to general stress.
-Should be a value between 0 and 1. Smaller or bigger values are treated as
-0 or 1 respectively.}
+\item{effect}{One or more effect values to convert to general stress. Should
+be a value between 0 and 1. Smaller or bigger values are treated as 0 or 1
+respectively.}
 
-\item{p, q}{The shape parameters of the beta distribution. Default is 3.2.}
+\item{p, q}{The shape parameters of the \code{\link[stats:Beta]{beta}}
+distribution. Default is 3.2.}
 
 \item{stress}{One or more stress values to convert to effect.}
 }
@@ -27,7 +28,7 @@ distribution.
 These are simple wrappers around the beta distribution function
 \code{\link[stats:Beta]{pbeta}} and the beta quantile function
 \code{\link[stats:Beta]{qbeta}}. \code{stress_to_effect} returns
-\code{1 - pbeta(stress, p, q)}. \code{effect_to_stress} first clips the
+\code{1 - pbeta(stress, p, q)}. \code{effect_to_stress} first clamps the
 effect to the interval [0, 1] and then returns
 \code{qbeta(1 - effect, p, q)}.
 
diff --git a/man/ec.Rd b/man/ec.Rd
index f5dd80b..1a9c828 100644
--- a/man/ec.Rd
+++ b/man/ec.Rd
@@ -4,40 +4,59 @@
 \alias{ec}
 \title{Effect Concentrations}
 \usage{
-ec(model, effect_name, target_effect)
+ec(model, response_name, response_level)
 }
 \arguments{
-\item{model}{The object returned from \code{ecxsys()}.}
+\item{model}{This can be one of three types of objects: Either the output of
+\code{\link{ecxsys}} or the output of \code{\link{predict_ecxsys}} or a
+data frame with a "concentration" column and a \code{response_name} column.
+See the examples.}
 
-\item{effect_name}{The name of the effect for which you want to calculate the
-EC. Must be the name of a column in \code{model$curves}.}
+\item{response_name}{The name of the effect or stress for which you want to
+calculate the EC. Must be one of \code{colnames(model$curves)}.}
 
-\item{target_effect}{The desired effect percentage between 0 and 100. For
-example with the value 10 the function will return the EC10, i.e. the
-concentration where the response falls below 90 \% of the maximum possible
+\item{response_level}{The desired response level as a percentage between 0
+and 100. For example with the value 10 the function will return the EC10,
+i.e. the concentration where the response falls below 90 \% of the maximum
 response.}
 }
 \value{
-A list containing the effect concentration and the corresponding
-  effect.
+A list containing the response concentration and the corresponding
+  response value.
 }
 \description{
-Estimate the concentration to reach a certain effect relative to the control.
+Estimate the concentration to reach a certain effect or stress level relative
+to the control.
 }
 \details{
 If the response level occurs multiple times because of hormesis, which may
-happen for low values of \code{target_effect}, then the occurrence with the
+happen for low values of \code{response_level}, then the occurrence with the
 smallest concentration is returned.
 }
 \examples{
+# Calculate the EC_10, the concentration where the effect falls
+# below 90 \% of the effect in the control.
+
 model <- ecxsys(
     concentration = c(0, 0.03, 0.3, 3, 10),
     effect_tox_observed = c(85, 76, 94, 35, 0),
-    effect_tox_env_observed = c(24, 23, 32, 0, 0),
     hormesis_concentration = 0.3
 )
-# Calculate the EC_10, the concentration where the effect falls
-# below 90 \% of the effect in the control:
-ec_10 <- ec(model, "effect_tox_sys", 10)
+
+# using the ecxsys() output or the curves therein directly:
+ec(model, "effect_tox_sys", 10)
+ec(model$curves, "effect_tox_sys", 10)
+
+# using the output of predict_ecxsys() with custom concentrations:
+conc <- 10^seq(-9, 1, length.out = 1000)
+curves <- predict_ecxsys(model, conc)
+ec(curves, "effect_tox_sys", 10)
+
+# using a custom data frame:
+df_custom <- data.frame(
+    concentration = curves$concentration,
+    foo = curves$effect_tox_sys
+)
+ec(df_custom, "foo", 10)
 
 }
diff --git a/man/ecxsys.Rd b/man/ecxsys.Rd
index e9ce10d..ca2cabc 100644
--- a/man/ecxsys.Rd
+++ b/man/ecxsys.Rd
@@ -6,10 +6,9 @@
 \usage{
 ecxsys(
   concentration,
-  effect_tox_observed,
-  effect_tox_env_observed,
   hormesis_concentration,
-  hormesis_index,
+  effect_tox_observed,
+  effect_tox_env_observed = NULL,
   effect_max = 100,
   p = 3.2,
   q = 3.2
@@ -20,22 +19,15 @@ ecxsys(
 indicate the control. Should be sorted in ascending order, otherwise it
 will be sorted automatically.}
 
+\item{hormesis_concentration}{The concentration where the hormesis occurs.
+This is usually the concentration of the highest effect after the control.}
+
 \item{effect_tox_observed}{A vector of effect values observed at the given
 concentrations and in absence of environmental stress. Values must be
 between 0 and \code{effect_max}.}
 
 \item{effect_tox_env_observed}{Effect values observed in the presence of
-environmental stress. This argument is optional and can be left out to
-model without environmental stress. Values must be between 0 and
-\code{effect_max}.}
-
-\item{hormesis_concentration}{The concentration where the hormesis occurs.
-This is usually the concentration of the highest effect after the control.}
-
-\item{hormesis_index}{\strong{Deprecated, will be removed soon.} A single
-integer specifying the index of the hormesis concentration in the
-concentration vector. This argument exists for compatibility with older
-versions of this function.}
+environmental stress. Must be between 0 and \code{effect_max}.}
 
 \item{effect_max}{The maximum value the effect could possibly reach. For
 survival data in percent this should be 100 (the default).}
@@ -43,43 +35,10 @@ survival data in percent this should be 100 (the default).}
 \item{p, q}{The shape parameters of the beta distribution. Default is 3.2.}
 }
 \value{
-The result is a list containing many different objects with the most
-  important being \code{curves} and \code{fn}. You can use \code{fn()} to
-  calculate the curves at your concentrations of choice, see examples.
-  \code{curves} is a data frame with the following columns:
-  \describe{
-    \item{concentration}{Concentrations regularly spaced on a logarithmic
-    scale in the given concentration range. The control is approximated by
-    the lowest non-control concentration times 1e-7.}
-    \item{effect_tox_simple}{The five-parameter log-logistic model of the
-    effect derived from the observations under toxicant stress but without
-    environmental stress.}
-    \item{effect_tox}{Modeled effect resulting from toxicant and system
-    stress.}
-    \item{effect_tox_sys}{Modeled effect resulting from toxicant and system
-    stress.}
-    \item{stress_tox}{The toxicant stress.}
-    \item{sys_stress_tox}{System stress under toxicant stress conditions
-    without environmental stress.}
-    \item{stress_tox_sys}{The sum of \code{stress_tox} and
-    \code{sys_stress_tox}.}
-    \item{effect_tox_env_simple}{The five-parameter log-logistic model of the
-    effect derived from the observations under toxicant stress with
-    environmental stress.}
-    \item{effect_tox_env}{Modeled effect resulting from toxicant and
-    environmental stress.}
-    \item{effect_tox_env_sys}{Modeled effect resulting from toxicant,
-    environmental and system stress.}
-    \item{stress_env}{Environmental stress.}
-    \item{stress_tox_env}{The sum of toxicant and environmental stress.}
-    \item{sys_stress_tox_env}{System stress under toxicant and
-    environmental stress conditions.}
-    \item{stress_tox_env_sys}{The sum of \code{stress_tox_env} and
-    \code{sys_stress_tox_env}.}
-    \item{use_for_plotting}{A boolean vector which is used in the plotting
-    functions. It controls which parts of the curves are removed for the
-    broken concentration axis.}
-  }
+A list (of class ecxsys) containing many different objects with the
+  most important being \code{curves}, a data frame containing effect and
+  stress values at different concentrations. See \code{\link{predict_ecxsys}}
+  for details.
 }
 \description{
 The ECx-SyS model for modeling concentration-effect relationships which
@@ -92,29 +51,25 @@ at or close to zero. If the model does not fit properly try adding an effect
 of 0 at ten times the maximum observed concentration.
 
 The vectors \code{concentration}, \code{effect_tox_observed} and
-\code{effect_tox_env_observed} must be of equal length and should be sorted
-by increasing concentration.
+\code{effect_tox_env_observed} (if provided) must be of equal length and
+sorted by increasing concentration.
 }
 \examples{
 model <- ecxsys(
     concentration = c(0, 0.03, 0.3, 3, 10),
+    hormesis_concentration = 0.3,
     effect_tox_observed = c(85, 76, 94, 35, 0),
-    effect_tox_env_observed = c(24, 23, 32, 0, 0),
-    hormesis_concentration = 0.3
+    effect_tox_env_observed = c(24, 23, 32, 0, 0)
 )
 
 # Use effect_max if for example the effect is given as the number of
 # surviving animals and the initial number of animals is 20:
 model <- ecxsys(
     concentration = c(0, 0.03, 0.3, 3, 10),
+    hormesis_concentration = 0.3,
     effect_tox_observed = c(17, 15.2, 18.8, 7, 0),
     effect_tox_env_observed = c(4.8, 4.6, 6.4, 0, 0),
-    hormesis_concentration = 0.3,
     effect_max = 20
 )
 
-# The returned object contains a function which is useful for calculating
-# effect and stress values at specific concentrations:
-model$fn(c(0, 0.01, 0.1, 1))
-
 }
diff --git a/man/plot_ecxsys.Rd b/man/plot_ecxsys.Rd
index 6af7b2e..b5d862e 100644
--- a/man/plot_ecxsys.Rd
+++ b/man/plot_ecxsys.Rd
@@ -1,26 +1,25 @@
 % Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/plot_ecxsys.R, R/plot_effect.R,
-%   R/plot_system_stress.R
+% Please edit documentation in R/plot_ecxsys.R, R/plot_effect.R, R/plot_stress.R
 \name{plot_ecxsys}
 \alias{plot_ecxsys}
 \alias{plot_effect}
-\alias{plot_system_stress}
+\alias{plot_stress}
 \title{Plot the results of the ECx-SyS model}
 \usage{
 plot_effect(
   model,
-  show_simple_model = FALSE,
+  show_LL5_model = FALSE,
   show_legend = FALSE,
   xlab = "concentration",
   ylab = "effect"
 )
 
-plot_system_stress(model, show_legend = FALSE)
+plot_stress(model, show_legend = FALSE)
 }
 \arguments{
-\item{model}{The list returned from ecxsys().}
+\item{model}{The list returned from \code{\link{ecxsys}}.}
 
-\item{show_simple_model}{Should the log-logistic models be plotted for
+\item{show_LL5_model}{Should the log-logistic models be plotted for
 comparison? Defaults to \code{FALSE}.}
 
 \item{show_legend}{Should the plot include a legend? Defaults to FALSE
@@ -34,11 +33,11 @@ system stress with and without environmental stress.
 }
 \examples{
 model <- ecxsys(
-    effect_tox_observed = c(85, 76, 94, 35, 0),
-    effect_tox_env_observed = c(24, 23, 32, 0, 0),
     concentration = c(0, 0.03, 0.3, 3, 10),
-    hormesis_concentration = 0.3
+    hormesis_concentration = 0.3,
+    effect_tox_observed = c(85, 76, 94, 35, 0),
+    effect_tox_env_observed = c(24, 23, 32, 0, 0)
 )
 plot_effect(model)
-plot_system_stress(model)
+plot_stress(model)
 }
diff --git a/man/predict_ecxsys.Rd b/man/predict_ecxsys.Rd
new file mode 100644
index 0000000..194496d
--- /dev/null
+++ b/man/predict_ecxsys.Rd
@@ -0,0 +1,59 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/predict_ecxsys.R
+\name{predict_ecxsys}
+\alias{predict_ecxsys}
+\title{Predict ECxSyS at various concentrations}
+\usage{
+predict_ecxsys(model, concentration)
+}
+\arguments{
+\item{model}{The output of a call to \code{\link{ecxsys}}.}
+
+\item{concentration}{A numeric vector of concentrations.}
+}
+\value{
+A data frame (of class "ecxsys_predicted") with the following
+  columns:
+  \describe{
+    \item{concentration}{Concentrations regularly spaced on a logarithmic
+    scale in the given concentration range. The control is approximated by
+    the lowest non-control concentration times 1e-7.}
+    \item{effect_tox_LL5}{The five-parameter log-logistic model of the
+    effect derived from the observations under toxicant stress but without
+    environmental stress.}
+    \item{effect_tox}{Modeled effect resulting from toxicant and system
+    stress.}
+    \item{effect_tox_sys}{Modeled effect resulting from toxicant and system
+    stress.}
+    \item{stress_tox}{The toxicant stress.}
+    \item{sys_tox}{System stress under toxicant stress conditions
+    without environmental stress.}
+    \item{stress_tox_sys}{The sum of \code{stress_tox} and
+    \code{sys_tox}.}
+    \item{effect_tox_env_LL5}{The five-parameter log-logistic model of the
+    effect derived from the observations under toxicant stress with
+    environmental stress.}
+    \item{effect_tox_env}{Modeled effect resulting from toxicant and
+    environmental stress.}
+    \item{effect_tox_env_sys}{Modeled effect resulting from toxicant,
+    environmental and system stress.}
+    \item{stress_env}{Environmental stress.}
+    \item{stress_tox_env}{The sum of toxicant and environmental stress.}
+    \item{sys_tox_env}{System stress under toxicant and
+    environmental stress conditions.}
+    \item{stress_tox_env_sys}{The sum of \code{stress_tox_env} and
+    \code{sys_tox_env}.}
+  }
+}
+\description{
+Predict ECxSyS at various concentrations
+}
+\examples{
+model <- ecxsys(
+    concentration = c(0, 0.03, 0.3, 3, 10),
+    hormesis_concentration = 0.3,
+    effect_tox_observed = c(85, 76, 94, 35, 0)
+)
+p <- predict_ecxsys(model, c(0.001, 0.01, 0.1, 1, 10))
+
+}
diff --git a/tests/testthat/test-ec.R b/tests/testthat/test-ec.R
index dc1e3ae..f5deb23 100644
--- a/tests/testthat/test-ec.R
+++ b/tests/testthat/test-ec.R
@@ -1,43 +1,80 @@
 context("effect concentration")
 
 
+test_that("all input formats produce identical models", {
+    model <- ecxsys(
+        concentration = c(0, 0.03, 0.3, 3, 10),
+        hormesis_concentration = 0.3,
+        effect_tox_observed = c(85, 76, 94, 35, 0)
+    )
+
+    # using the ecxsys() output or the curves therein directly:
+    ec10_a <- ec(model, "effect_tox_sys", 10)
+    ec10_b <- ec(model$curves, "effect_tox_sys", 10)
+
+    # using the output of predict_ecxsys() with custom concentrations:
+    conc <- 10^seq(-9, 1, length.out = 1000)
+    curves <- predict_ecxsys(model, conc)
+    ec10_c <- ec(curves, "effect_tox_sys", 10)
+
+    # using a custom data frame:
+    df_custom <- data.frame(
+        concentration = curves$concentration,
+        foo = curves$effect_tox_sys
+    )
+    ec10_d <- ec(df_custom, "foo", 10)
+
+    expect_identical(ec10_a, ec10_b)
+    expect_identical(ec10_b, ec10_c)
+    expect_identical(ec10_c, ec10_d)
+})
+
+
 test_that("ec values have not changed", {
-    result <- ecxsys(
+    model <- ecxsys(
         concentration = c(0, 0.03, 0.3, 3, 10),
+        hormesis_concentration = 0.3,
         effect_tox_observed = c(85, 76, 94, 35, 0),
-        effect_tox_env_observed = c(24, 23, 32, 0, 0),
-        hormesis_index = 3
+        effect_tox_env_observed = c(24, 23, 32, 0, 0)
     )
     expect_equal(
-        ec(result, "effect_tox_sys",50),
-        list(effect = 42.38984413, concentration = 2.547711893)
+        ec(model, "effect_tox_sys", 50),
+        list(response_value = 42.389844, concentration = 2.547712),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_sys", 10),
-        list(effect = 76.30171944, concentration = 1.015730561)
+        ec(model, "effect_tox_sys", 10),
+        list(response_value = 76.301719, concentration = 1.015731),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_sys", 5),
-        list(effect = 80.54070385, concentration = 0.003172831366)
+        ec(model, "effect_tox_sys", 5),
+        list(response_value = 80.540705, concentration = 0.003173),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_sys", 1),
-        list(effect = 83.93189138, concentration = 0.00005727730324)
+        ec(model, "effect_tox_sys", 1),
+        list(response_value = 83.931891, concentration = 0.000057),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_env_sys", 50),
-        list(effect = 12.1555922, concentration = 0.9058480238)
+        ec(model, "effect_tox_env_sys", 50),
+        list(response_value = 12.155592, concentration = 0.905848),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_env_sys", 10),
-        list(effect = 21.88006596, concentration = 0.0007604409081)
+        ec(model, "effect_tox_env_sys", 10),
+        list(response_value = 21.880066, concentration = 0.0007604),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_env_sys", 5),
-        list(effect = 23.09562518, concentration = 0.0001004406979)
+        ec(model, "effect_tox_env_sys", 5),
+        list(response_value = 23.095625, concentration = 0.00010044),
+        tolerance = 1e-4
     )
     expect_equal(
-        ec(result, "effect_tox_env_sys", 1),
-        list(effect = 24.06807255, concentration = 0.000001640834495)
+        ec(model, "effect_tox_env_sys", 1),
+        list(response_value = 24.068073, concentration = 0.000002),
+        tolerance = 1e-4
     )
 })
diff --git a/tests/testthat/test-ecxsys.R b/tests/testthat/test-ecxsys.R
index 3195874..1469b47 100644
--- a/tests/testthat/test-ecxsys.R
+++ b/tests/testthat/test-ecxsys.R
@@ -3,65 +3,67 @@ context("ecxsys")
 
 # Create model here for use in the various tests to save typing:
 mod <- ecxsys(
-    effect_tox_observed = c(85, 76, 94, 35, 0),
-    effect_tox_env_observed = c(24, 23, 32, 0, 0),
     concentration = c(0, 0.03, 0.3, 3, 10),
-    hormesis_index = 3
-    # hormesis_concentration = 0.3
+    hormesis_concentration = 0.3,
+    effect_tox_observed = c(85, 76, 94, 35, 0),
+    effect_tox_env_observed = c(24, 23, 32, 0, 0)
 )
 
 
-test_that("error when both hormesis arguments passed to ecxsys", {
+test_that("error when hormesis_concentration not in concentration", {
+    errorm <- "hormesis_concentration must equal one of the concentration values."
     expect_error(
         ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
             concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_index = 3,
-            hormesis_concentration = 0.3
-        )
+            hormesis_concentration = 0.4,
+            effect_tox_observed = c(85, 76, 94, 35, 0)
+        ),
+        errorm,
+        fixed = TRUE
     )
-})
-
-
-test_that("error when hormesis_concentration not in concentration", {
     expect_error(
         ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
             concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_concentration = 0.4
-        )
+            hormesis_concentration = 30,
+            effect_tox_observed = c(85, 76, 94, 35, 0)
+        ),
+        errorm,
+        fixed = TRUE
     )
 })
 
 
 test_that("error when hormesis_index <= 2 or >= (length(concentration))", {
-    expect_error(
-        ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
-            concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_index = 1
-        )
+    errorm <- paste(
+        "hormesis_concentration must be greater than the lowest",
+        "non-control concentration and less than the highest concentration"
     )
     expect_error(
         ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
             concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_index = 2
-        )
+            hormesis_concentration = 0,
+            effect_tox_observed = c(85, 76, 94, 35, 0)
+        ),
+        errorm,
+        fixed = TRUE
     )
     expect_error(
         ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
             concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_index = 5
-        )
+            hormesis_concentration = 0.03,
+            effect_tox_observed = c(85, 76, 94, 35, 0)
+        ),
+        errorm,
+        fixed = TRUE
     )
     expect_error(
         ecxsys(
-            effect_tox_observed = c(85, 76, 94, 35, 0),
             concentration = c(0, 0.03, 0.3, 3, 10),
-            hormesis_index = 6
-        )
+            hormesis_concentration = 10,
+            effect_tox_observed = c(85, 76, 94, 35, 0)
+        ),
+        errorm,
+        fixed = TRUE
     )
 })
 
@@ -73,112 +75,81 @@ test_that("min(concentration) == 0 is shifted the correct amount", {
 
 test_that("the discrete results have not changed", {
     expect_equal(
-        round(mod$effect_tox_simple, 3),
-        c(85.000, 84.542, 78.946, 31.959, 2.610)
+        mod$effect_tox_LL5,
+        c(85, 84.5420, 78.9464, 31.9586, 2.6096),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$effect_tox, 3),
-        c(100.000, 99.676, 94.320, 34.860, 0.840)
+        mod$effect_tox,
+        c(100, 99.6760, 94.3198, 34.8601, 0.8400),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$stress_tox, 3),
-        c(0.000, 0.078, 0.207, 0.579, 0.893)
+        mod$stress_tox,
+        c(0, 0.0784, 0.2067, 0.5794, 0.8927),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$sys_stress_tox, 3),
-        c(0.296, 0.280, 0.000, 0.000, 0.000)
+        mod$sys_tox_not_fitted,
+        c(0.2965, 0.2795, 0, 0, 0),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$effect_tox_sys, 3),
-        c(84.791, 77.481, 93.138, 34.86, 0.84)
+        mod$effect_tox_sys,
+        c(84.7912, 77.4811, 93.1380, 34.8602, 0.8400),
+        tolerance = 1e-4
     )
-    expect_equal(round(mod$stress_env, 3), 0.388)
+    expect_equal(mod$stress_env, 0.3885, tolerance = 1e-3)
     expect_equal(
-        round(mod$effect_tox_env_simple, 3),
-        c(26.333, 26.333, 26.062, 0.710, 0.037)
+        mod$effect_tox_env_LL5,
+        c(26.3333, 26.3333, 26.0620, 0.7104, 0.0374),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$stress_tox_env, 3),
-        c(0.388, 0.467, 0.595, 0.968, 1.281)
+        mod$stress_tox_env,
+        c(0.3884, 0.4669, 0.5952, 0.9679, 1.2812),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$effect_tox_env, 3),
-        c(70.879, 56.414, 32.000, 0.020, 0.000)
+        mod$effect_tox_env,
+        c(70.8786, 56.4138, 32, 0.0202, 0),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$sys_stress_tox_env, 3),
-        c(0.254, 0.181, 0.000, 0.000, 0.000)
+        mod$sys_tox_env_not_fitted,
+        c(0.2537, 0.1815, 0, 0, 0),
+        tolerance = 1e-4
     )
     expect_equal(
-        round(mod$effect_tox_env_sys, 3),
-        c(24.325, 22.323, 29.386, 0.020, 0.000)
+        mod$effect_tox_env_sys,
+        c(24.3254, 22.3232, 29.3860, 0.0202, 0),
+        tolerance = 1e-4
     )
 })
 
 
-test_that("the smooth curves have not changed", {
-    curves <- mod$curves
-    i <- c(1, 714, 810, 905, 1000)
-    expect_equal(
-        round(curves$concentration[i], 3),
-        c(0.000, 0.014, 0.125, 1.120, 10.000)
-    )
-    expect_equal(
-        round(curves$effect_tox_simple[i], 3),
-        c(85.000, 84.812, 82.700, 61.288, 2.610)
-    )
-    expect_equal(
-        round(curves$effect_tox[i], 3),
-        c(100.000, 99.879, 98.064, 73.665, 0.840)
-    )
-    expect_equal(
-        round(curves$effect_tox_sys[i], 3),
-        c(84.780, 77.932, 87.580, 73.665, 0.840)
-    )
-    expect_equal(
-        round(curves$stress_tox[i], 3),
-        c(0.000, 0.057, 0.142, 0.372, 0.893)
-    )
-    expect_equal(
-        round(curves$sys_stress_tox[i], 3),
-        c(0.298, 0.289, 0.134, 0.000, 0.000)
-    )
-    expect_equal(
-        round(curves$stress_tox_sys[i], 3),
-        c(0.298, 0.346, 0.276, 0.372, 0.893)
-    )
-    expect_equal(
-        round(curves$effect_tox_env[i], 3),
-        c(70.863, 60.496, 44.083, 8.514, 0)
-    )
-    expect_equal(
-        round(curves$effect_tox_env_sys[i], 3),
-        c(24.311, 20.727, 29.948, 8.513, 0.000)
-    )
-    expect_equal(
-        round(curves$stress_env[i], 3),
-        c(0.388, 0.388, 0.388, 0.388, 0.388)
-    )
-    expect_equal(
-        round(curves$effect_tox_env_simple[i], 3),
-        c(26.333, 26.333, 26.329, 7.538, 0.037)
-    )
-    expect_equal(
-        round(curves$stress_tox_env[i], 3),
-        c(0.389, 0.445, 0.531, 0.761, 1.281)
-    )
-    expect_equal(
-        round(curves$sys_stress_tox_env[i], 3),
-        c(0.252, 0.218, 0.076, 0.000, 0.000)
-    )
-    expect_equal(
-        round(curves$stress_tox_env_sys[i], 3),
-        c(0.640, 0.663, 0.607, 0.761, 1.281)
-    )
-    expect_equal(
-        curves$use_for_plotting[i],
-        c(TRUE, TRUE, TRUE, TRUE, TRUE)
-    )
+test_that("the curves have not changed", {
+    new_curves <- mod$curves[c(1, 714, 810, 905, 1000), ]  # random indices
+    rownames(new_curves) <- NULL
+    reference_curves <- data.frame(
+        concentration = c(0.0000, 0.0137, 0.1253, 1.1196, 10.0000),
+        effect_tox_LL5 = c(85.0000, 84.8116, 82.6996, 61.2882, 2.6096),
+        effect_tox_env_LL5 = c(26.3333, 26.3333, 26.3289, 7.5384, 0.0374),
+        effect_tox = c(100.0000, 99.8787, 98.0645, 73.6652, 0.8400),
+        stress_tox = c(0.0001, 0.0570, 0.1421, 0.3721, 0.8927),
+        sys_tox = c(0.2980, 0.2887, 0.1336, 0.0000, 0.0000),
+        stress_tox_sys = c(0.2981, 0.3457, 0.2757, 0.3721, 0.8927),
+        effect_tox_sys = c(84.7797, 77.9323, 87.5799, 73.6652, 0.8400),
+        stress_env = c(0.3885, 0.3885, 0.3885, 0.3885, 0.3885),
+        stress_tox_env = c(0.3886, 0.4455, 0.5306, 0.7606, 1.2812),
+        effect_tox_env = c(70.8634, 60.4957, 44.0834, 8.5137, 0.0000),
+        sys_tox_env = c(0.2516, 0.2175, 0.0762, 0.0000, 0.0000),
+        stress_tox_env_sys = c(0.6402, 0.6630, 0.6068, 0.7606, 1.2812),
+        effect_tox_env_sys = c(24.3112, 20.7272, 29.9481, 8.5133, 0.0000),
+        use_for_plotting = c(TRUE, TRUE, TRUE, TRUE, TRUE)
+    )
+    class(new_curves) <- class(reference_curves)
+    expect_equal(new_curves, reference_curves, tolerance = 1e-3)
 })
 
 
@@ -186,36 +157,38 @@ test_that("the returned fn works the same way as internally", {
     # I don't know why it would fail but it doesn't hurt to test it.
     curves <- mod$curves
     curves$use_for_plotting <- NULL  # remove column "use_for_plotting"
-    expect_identical(curves, mod$fn(curves$concentration))
+    expect_identical(curves, predict_ecxsys(mod, curves$concentration))
 })
 
 
 test_that("function arguments are returned unchanged", {
-    expect_equal(mod$args$effect_tox_observed, c(85, 76, 94, 35, 0))
-    expect_equal(mod$args$effect_tox_env_observed, c(24, 23, 32, 0, 0))
-    expect_equal(mod$args$concentration, c(0, 0.03, 0.3, 3, 10))
-    expect_equal(mod$args$hormesis_index, 3)
-    # expect_equal(mod$args$hormesis_concentration, 0.3)
-    expect_equal(mod$args$effect_max, 100)
-    expect_equal(mod$args$p, 3.2)
-    expect_equal(mod$args$q, 3.2)
+    args_reference <- list(
+        concentration = c(0, 0.03, 0.3, 3, 10),
+        hormesis_concentration = 0.3,
+        effect_tox_observed = c(85, 76, 94, 35, 0),
+        effect_tox_env_observed = c(24, 23, 32, 0, 0),
+        effect_max = 100,
+        p = 3.2,
+        q = 3.2
+    )
+    expect_identical(args_reference, mod$args)
 })
 
 
 test_that("results are independent of concentration shift", {
     mod_2 <- ecxsys(
-        effect_tox_observed = c(85, 76, 94, 35, 0),
-        effect_tox_env_observed = c(24, 23, 32, 0, 0),
         concentration = c(0, 0.03, 0.3, 3, 10) * 2,
-        hormesis_index = 3
+        hormesis_concentration = 0.3 * 2,
+        effect_tox_observed = c(85, 76, 94, 35, 0),
+        effect_tox_env_observed = c(24, 23, 32, 0, 0)
     )
     expect_equal(mod$effect_tox_sys, mod_2$effect_tox_sys)
     expect_equal(mod$effect_tox_env_sys, mod_2$effect_tox_env_sys)
     mod_10 <- ecxsys(
-        effect_tox_observed = c(85, 76, 94, 35, 0),
-        effect_tox_env_observed = c(24, 23, 32, 0, 0),
         concentration = c(0, 0.03, 0.3, 3, 10) * 10,
-        hormesis_index = 3
+        hormesis_concentration = 0.3 * 10,
+        effect_tox_observed = c(85, 76, 94, 35, 0),
+        effect_tox_env_observed = c(24, 23, 32, 0, 0)
     )
     # Concentration shifts by factors other than powers of 10 will affect
     # the result because of the way the zero concentration is "corrected".
@@ -230,9 +203,9 @@ test_that("results are independent of concentration shift", {
 
 test_that("effect_tox_env_observed can be left out", {
     mod_without_env <- ecxsys(
-        effect_tox_observed = c(85, 76, 94, 35, 0),
         concentration = c(0, 0.03, 0.3, 3, 10),
-        hormesis_index = 3
+        hormesis_concentration = 0.3,
+        effect_tox_observed = c(85, 76, 94, 35, 0)
     )
     expect_equal(mod$effect_tox_sys, mod_without_env$effect_tox_sys)
     expect_equal(mod$curves$effect_tox_sys,
@@ -240,12 +213,17 @@ test_that("effect_tox_env_observed can be left out", {
 })
 
 
-test_that("model not converging produces warnings", {
+test_that("sys model not converging produces a warning, not an error", {
     expect_warning(
         ecxsys(
             concentration = c(0, 0.1, 0.5, 1, 10, 33),
-            effect_tox_observed = c(98, 98, 96, 76, 26, 0),
-            hormesis_index = 3
-        )
+            hormesis_concentration = 0.5,
+            effect_tox_observed = c(98, 98, 96, 76, 26, 0)
+        ),
+        paste(
+            "Using a horizontal linear model for sys_tox_mod because the",
+            "Weibull model did not converge."
+        ),
+        fixed = TRUE
     )
 })
diff --git a/tests/testthat/test-stress_effect_conversion.R b/tests/testthat/test-stress_effect_conversion.R
index abd1317..506491d 100644
--- a/tests/testthat/test-stress_effect_conversion.R
+++ b/tests/testthat/test-stress_effect_conversion.R
@@ -5,16 +5,16 @@ test_that("stress_to_effect handles bad input", {
     expect_equal(stress_to_effect(-3), 1)
     expect_equal(stress_to_effect(10), 0)
     expect_error(stress_to_effect("a"))
-    expect_warning(stress_to_effect(0.5, -3, 10))
-    expect_warning(stress_to_effect(0.5, 3, -10))
+    expect_error(stress_to_effect(0.5, -3, 10))
+    expect_error(stress_to_effect(0.5, 3, -10))
 })
 
 test_that("effect_to_stress handles bad input", {
     expect_equal(effect_to_stress(-15), 1)
     expect_equal(effect_to_stress(20), 0)
     expect_error(effect_to_stress("a"))
-    expect_warning(effect_to_stress(0.5, -3, 10))
-    expect_warning(effect_to_stress(0.5, 3, -10))
+    expect_error(effect_to_stress(0.5, -3, 10))
+    expect_error(effect_to_stress(0.5, 3, -10))
 })
 
 test_that("stress_to_effect is correct", {
-- 
GitLab