utils::globalVariables(c("type", "variable", "observed")) #' Plot the observed data and the fitted model of an mkinfit object #' #' Solves the differential equations with the optimised and fixed parameters #' from a previous successful call to \code{\link{mkinfit}} and plots the #' observed data together with the solution of the fitted model. #' #' If the current plot device is a \code{\link[tikzDevice]{tikz}} device, then #' latex is being used for the formatting of the chi2 error level, if #' \code{show_errmin = TRUE}. #' #' @aliases plot.mkinfit plot_sep plot_res plot_err #' @param x Alias for fit introduced for compatibility with the generic S3 #' method. #' @param fit An object of class \code{\link{mkinfit}}. #' @param obs_vars A character vector of names of the observed variables for #' which the data and the model should be plotted. Defauls to all observed #' variables in the model. #' @param xlab Label for the x axis. #' @param ylab Label for the y axis. #' @param xlim Plot range in x direction. #' @param ylim Plot range in y direction. If given as a list, plot ranges #' for the different plot rows can be given for row layout. #' @param col_obs Colors used for plotting the observed data and the #' corresponding model prediction lines. #' @param pch_obs Symbols to be used for plotting the data. #' @param lty_obs Line types to be used for the model predictions. #' @param add Should the plot be added to an existing plot? #' @param legend Should a legend be added to the plot? #' @param show_residuals Should residuals be shown? If only one plot of the #' fits is shown, the residual plot is in the lower third of the plot. #' Otherwise, i.e. if "sep_obs" is given, the residual plots will be located #' to the right of the plots of the fitted curves. If this is set to #' 'standardized', a plot of the residuals divided by the standard deviation #' given by the fitted error model will be shown. #' @param standardized When calling 'plot_res', should the residuals be #' standardized in the residual plot? #' @param show_errplot Should squared residuals and the error model be shown? #' If only one plot of the fits is shown, this plot is in the lower third of #' the plot. Otherwise, i.e. if "sep_obs" is given, the residual plots will #' be located to the right of the plots of the fitted curves. #' @param maxabs Maximum absolute value of the residuals. This is used for the #' scaling of the y axis and defaults to "auto". #' @param sep_obs Should the observed variables be shown in separate subplots? #' If yes, residual plots requested by "show_residuals" will be shown next #' to, not below the plot of the fits. #' @param rel.height.middle The relative height of the middle plot, if more #' than two rows of plots are shown. #' @param row_layout Should we use a row layout where the residual plot or the #' error model plot is shown to the right? #' @param lpos Position(s) of the legend(s). Passed to \code{\link{legend}} as #' the first argument. If not length one, this should be of the same length #' as the obs_var argument. #' @param inset Passed to \code{\link{legend}} if applicable. #' @param show_errmin Should the FOCUS chi2 error value be shown in the upper #' margin of the plot? #' @param errmin_digits The number of significant digits for rounding the FOCUS #' chi2 error percentage. #' @param frame Should a frame be drawn around the plots? #' @param \dots Further arguments passed to \code{\link{plot}}. #' @import graphics #' @importFrom grDevices dev.cur #' @return The function is called for its side effect. #' @author Johannes Ranke #' @examples #' #' # One parent compound, one metabolite, both single first order, path from #' # parent to sink included #' \dontrun{ #' SFO_SFO <- mkinmod(parent = mkinsub("SFO", "m1", full = "Parent"), #' m1 = mkinsub("SFO", full = "Metabolite M1" )) #' fit <- mkinfit(SFO_SFO, FOCUS_2006_D, quiet = TRUE) #' fit <- mkinfit(SFO_SFO, FOCUS_2006_D, quiet = TRUE, error_model = "tc") #' plot(fit) #' plot_res(fit) #' plot_res(fit, standardized = FALSE) #' plot_err(fit) #' #' # Show the observed variables separately, with residuals #' plot(fit, sep_obs = TRUE, show_residuals = TRUE, lpos = c("topright", "bottomright"), #' show_errmin = TRUE) #' #' # The same can be obtained with less typing, using the convenience function plot_sep #' plot_sep(fit, lpos = c("topright", "bottomright")) #' #' # Show the observed variables separately, with the error model #' plot(fit, sep_obs = TRUE, show_errplot = TRUE, lpos = c("topright", "bottomright"), #' show_errmin = TRUE) #' } #' #' @export plot.mkinfit <- function(x, fit = x, obs_vars = names(fit$mkinmod$map), xlab = "Time", ylab = "Residue", xlim = range(fit$data$time), ylim = "default", col_obs = 1:length(obs_vars), pch_obs = col_obs, lty_obs = rep(1, length(obs_vars)), add = FALSE, legend = !add, show_residuals = FALSE, show_errplot = FALSE, maxabs = "auto", sep_obs = FALSE, rel.height.middle = 0.9, row_layout = FALSE, lpos = "topright", inset = c(0.05, 0.05), show_errmin = FALSE, errmin_digits = 3, frame = TRUE, ...) { if (identical(show_residuals, "standardized")) { show_residuals <- TRUE standardized <- TRUE } else { standardized <- FALSE } if (add && show_residuals) stop("If adding to an existing plot we can not show residuals") if (add && show_errplot) stop("If adding to an existing plot we can not show the error model plot") if (show_residuals && show_errplot) stop("We can either show residuals over time or the error model plot, not both") if (add && sep_obs) stop("If adding to an existing plot we can not show observed variables separately") solution_type = fit$solution_type parms.all <- c(fit$bparms.optim, fit$bparms.fixed) ininames <- c( rownames(subset(fit$start, type == "state")), rownames(subset(fit$fixed, type == "state"))) odeini <- parms.all[ininames] # Order initial state variables names(odeini) <- sub("_0", "", names(odeini)) odeini <- odeini[names(fit$mkinmod$diffs)] outtimes <- seq(xlim[1], xlim[2], length.out=100) odenames <- c( rownames(subset(fit$start, type == "deparm")), rownames(subset(fit$fixed, type == "deparm"))) odeparms <- parms.all[odenames] if (solution_type == "deSolve" & !is.null(fit$mkinmod$cf)) { fit$mkinmod[["symbols"]] <- deSolve::checkDLL(dllname = fit$mkinmod$dll_info[["name"]], func = "diffs", initfunc = "initpar", jacfunc = NULL, nout = 0, outnames = NULL) } out <- mkinpredict(fit$mkinmod, odeparms, odeini, outtimes, solution_type = solution_type, atol = fit$atol, rtol = fit$rtol) out <- as.data.frame(out) names(col_obs) <- names(pch_obs) <- names(lty_obs) <- obs_vars # Create a plot layout only if not to be added to an existing plot # or only a single plot is requested (e.g. by plot.mmkin) do_layout = FALSE if (show_residuals | sep_obs | show_errplot) do_layout = TRUE n_plot_rows = if (sep_obs) length(obs_vars) else 1 if (do_layout) { # Layout should be restored afterwards oldpar <- par(no.readonly = TRUE) on.exit(par(oldpar, no.readonly = TRUE)) # If the observed variables are shown separately, or if requested, do row layout if (sep_obs | row_layout) { row_layout <- TRUE n_plot_cols = if (show_residuals | show_errplot) 2 else 1 n_plots = n_plot_rows * n_plot_cols # Set relative plot heights, so the first and the last plot are the norm # and the middle plots (if n_plot_rows >2) are smaller by rel.height.middle rel.heights <- if (n_plot_rows > 2) c(1, rep(rel.height.middle, n_plot_rows - 2), 1) else rep(1, n_plot_rows) layout_matrix = matrix(1:n_plots, n_plot_rows, n_plot_cols, byrow = TRUE) layout(layout_matrix, heights = rel.heights) } else { # else show residuals in the lower third to keep compatibility layout(matrix(c(1, 2), 2, 1), heights = c(2, 1.3)) par(mar = c(3, 4, 4, 2) + 0.1) } } # Replicate legend position argument if necessary if (length(lpos) == 1) lpos = rep(lpos, n_plot_rows) # Loop over plot rows for (plot_row in 1:n_plot_rows) { row_obs_vars = if (sep_obs) obs_vars[plot_row] else obs_vars # Set ylim to sensible default, or to the specified value if (is.list(ylim)) { ylim_row <- ylim[[plot_row]] } else { if (ylim[[1]] == "default") { ylim_row = c(0, max(c(subset(fit$data, variable %in% row_obs_vars)$observed, unlist(out[row_obs_vars])), na.rm = TRUE)) } else { ylim_row = ylim } } if (row_layout) { # Margins for top row of plots when we have more than one row # Reduce bottom margin by 2.1 - hides x axis legend if (plot_row == 1 & n_plot_rows > 1) { par(mar = c(3.0, 4.1, 4.1, 2.1)) } # Margins for middle rows of plots, if any if (plot_row > 1 & plot_row < n_plot_rows) { # Reduce top margin by 2 after the first plot as we have no main title, # reduced plot height, therefore we need rel.height.middle in the layout par(mar = c(3.0, 4.1, 2.1, 2.1)) } # Margins for bottom row of plots when we have more than one row if (plot_row == n_plot_rows & n_plot_rows > 1) { # Restore bottom margin for last plot to show x axis legend par(mar = c(5.1, 4.1, 2.1, 2.1)) } } # Set up the main plot if not to be added to an existing plot if (add == FALSE) { plot(0, type="n", xlim = xlim, ylim = ylim_row, xlab = xlab, ylab = ylab, frame = frame, ...) } # Plot the data for (obs_var in row_obs_vars) { points(subset(fit$data, variable == obs_var, c(time, observed)), pch = pch_obs[obs_var], col = col_obs[obs_var]) } # Plot the model output matlines(out$time, out[row_obs_vars], col = col_obs[row_obs_vars], lty = lty_obs[row_obs_vars]) if (legend == TRUE) { # Get full names from model definition if they are available legend_names = lapply(row_obs_vars, function(x) { if (!is.null(fit$mkinmod$spec[[x]]$full_name)) if (is.na(fit$mkinmod$spec[[x]]$full_name)) x else fit$mkinmod$spec[[x]]$full_name else x }) legend(lpos[plot_row], inset= inset, legend = legend_names, col = col_obs[row_obs_vars], pch = pch_obs[row_obs_vars], lty = lty_obs[row_obs_vars]) } # Show chi2 error value if requested if (show_errmin) { if (length(row_obs_vars) == 1) { errmin_var = row_obs_vars } else { errmin_var = "All data" if (length(row_obs_vars) != length(fit$mkinmod$map)) { warning("Showing chi2 error level for all data, but only ", row_obs_vars, " were selected for plotting") } } chi2 <- signif(100 * mkinerrmin(fit)[errmin_var, "err.min"], errmin_digits) # Use LateX if the current plotting device is tikz if (names(dev.cur()) == "tikz output") { chi2_text <- paste0("$\\chi^2$ error level = ", chi2, "\\%") } else { chi2_perc <- paste0(chi2, "%") chi2_text <- bquote(chi^2 ~ "error level" == .(chi2_perc)) } mtext(chi2_text, cex = 0.7, line = 0.4) } if (do_layout & !row_layout) { par(mar = c(5, 4, 0, 2) + 0.1) } # Show residuals if requested if (show_residuals) { mkinresplot(fit, obs_vars = row_obs_vars, standardized = standardized, pch_obs = pch_obs[row_obs_vars], col_obs = col_obs[row_obs_vars], legend = FALSE, frame = frame, xlab = xlab, xlim = xlim, maxabs = maxabs) } # Show error model plot if requested if (show_errplot) { mkinerrplot(fit, obs_vars = row_obs_vars, pch_obs = pch_obs[row_obs_vars], col_obs = col_obs[row_obs_vars], legend = FALSE, frame = frame) } } } #' @rdname plot.mkinfit #' @export plot_sep <- function(fit, show_errmin = TRUE, show_residuals = ifelse(identical(fit$err_mod, "const"), TRUE, "standardized"), ...) { plot.mkinfit(fit, sep_obs = TRUE, show_residuals = show_residuals, show_errmin = show_errmin, ...) } #' @rdname plot.mkinfit #' @export plot_res <- function(fit, sep_obs = FALSE, show_errmin = sep_obs, standardized = ifelse(identical(fit$err_mod, "const"), FALSE, TRUE), ...) { plot.mkinfit(fit, sep_obs = sep_obs, show_errmin = show_errmin, show_residuals = ifelse(standardized, "standardized", TRUE), row_layout = TRUE, ...) } #' @rdname plot.mkinfit #' @export plot_err <- function(fit, sep_obs = FALSE, show_errmin = sep_obs, ...) { plot.mkinfit(fit, sep_obs = sep_obs, show_errmin = show_errmin, show_errplot = TRUE, row_layout = TRUE, ...) } #' Plot the observed data and the fitted model of an mkinfit object #' #' Deprecated function. It now only calls the plot method #' \code{\link{plot.mkinfit}}. #' #' @param fit an object of class \code{\link{mkinfit}}. #' @param \dots further arguments passed to \code{\link{plot.mkinfit}}. #' @return The function is called for its side effect. #' @author Johannes Ranke #' @export mkinplot <- function(fit, ...) { plot(fit, ...) }