R/ui_model-building.R

Defines functions print.networkModel add_covariates set_prior set_params set_obs set_init add_pulse_event set_half_life set_split set_steady set_topo set_size_family set_prop_family new_networkModel

Documented in add_covariates add_pulse_event new_networkModel print.networkModel set_half_life set_init set_obs set_params set_prior set_prop_family set_size_family set_split set_steady set_topo

### * All functions in this file are exported

### * new_networkModel()

#' Create an empty network model
#'
#' The first step in building a network model is to create a new, empty
#' \code{networkModel} object. This model can then be completed using functions
#' such as \code{set_topo()}, \code{set_init()}, etc...
#'
#' @param quiet Boolean, if \code{FALSE} print a message indicating which
#'     distribution family is used for proportions.
#'
#' @return An empty \code{networkModel} object. It is basically a zero-row
#'     tibble with the appropriate columns.
#' 
#' @examples
#' m <- new_networkModel()
#' m
#' class(m)
#'
#' @export

new_networkModel <- function(quiet = FALSE) {
    verbose <- !quiet
    x <- tibble::tibble(topology = list(),
                        initial = list(),
                        observations = list())
    attr(x, "prop_family") <- "gamma_cv"
    if (verbose) {
        message("Using default distribution family for proportions (\"gamma_cv\").")
        describe_z_eta("eta", attr(x, "prop_family"))
    }
    attr(x, "size_family") <- "normal_cv"
    if (verbose) {
        message("Using default distribution family for sizes (\"normal_cv\").")
        describe_z_eta("zeta", attr(x, "size_family"))
    }
    attr(x, "size_zeta_per_compartment") <- FALSE
    x <- structure(x, class = c("networkModel", class(x)))
    return(x)
}

### * set_prop_family()

#' Set the distribution family for observed proportions
#'
#' @param nm A \code{networkModel} object (output from
#'     \code{\link{new_networkModel}}).
#' @param family Allowed values are "gamma_cv", "beta_phi", "normal_cv", and
#'     "normal_sd".
#' @param quiet Boolean, if \code{FALSE} print a message indicating which
#'     distribution family is used for proportions.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' library(magrittr)
#' 
#' m <- new_networkModel() %>%
#'   set_topo(links = "NH4, NO3 -> epi -> pseph, tricor")
#' m <- m %>% set_prop_family("beta_phi")
#' m
#' attr(m, "prop_family")
#'
#' @export

set_prop_family <- function(nm, family, quiet = FALSE) {
    verbose <- !quiet
    known_families <- c("gamma_cv" = 1, "normal_cv" = 2, "normal_sd" = 3,
                        "beta_phi" = 4)
    if (!family %in% names(known_families)) {
        stop("Unknown distribution family for proportions. Got value: \"",
             family, "\"\n",
             "Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")),
                                            collapse = ", "))
    }
    attr(nm, "prop_family") <- family
    if (verbose) {
        message("Using distribution family for proportions: \"", family, "\".",
                sep = "")
        describe_z_eta("eta", family)
    }
    return(nm)
}

### * set_size_family()

#' Set the distribution family for observed sizes
#'
#' @param nm A \code{networkModel} object (output from
#'     \code{\link{new_networkModel}}).
#' @param family Allowed values are "normal_cv" and "normal_sd".
#' @param by_compartment Boolean, if TRUE then zeta is compartment-specific.
#' @param quiet Boolean, if \code{FALSE} print a message indicating which
#'     distribution family is used for proportions.
#' @param quiet_reset Boolean, write a message when model parameters (and
#'     covariates and priors) are reset?
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' library(magrittr)
#' 
#' m <- new_networkModel() %>%
#'   set_topo(links = "NH4, NO3 -> epi -> pseph, tricor")
#' m <- m %>% set_size_family("normal_sd")
#' m
#' attr(m, "size_family")
#'
#' m <- m %>% set_size_family(by_compartment = TRUE)
#' attr(m, "size_zeta_per_compartment")
#' 
#' @export

set_size_family <- function(nm, family, by_compartment, quiet = FALSE,
                            quiet_reset = FALSE) {
    verbose <- !quiet
    verbose_reset <- !quiet_reset
    if (!missing(family)) {
        known_families <- c("normal_cv" = 1, "normal_sd" = 2)
        if (!family %in% names(known_families)) {
            stop("Unknown distribution family for proportions. Got value: \"",
                 family, "\"\n",
                 "Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")),
                                                collapse = ", "))
        }
        attr(nm, "size_family") <- family
        if (verbose) {
            message("Using distribution family for sizes: \"", family, "\".",
                    sep = "")
            describe_z_eta("zeta", family)
        }
    }
    if (!missing(by_compartment)) {
        stopifnot(by_compartment %in% c(FALSE, TRUE))
        attr(nm, "size_zeta_per_compartment") <- by_compartment
        if(verbose) {
            message("Compartment-specific zeta set to ", by_compartment, ".",
                    sep = "")
        }
        if (!is.null(nm[["parameters"]])) {
            # nm already had parameters
            # Reset to update the zeta parameters
            nm <- add_param_mapping(nm)
            if (verbose_reset) {
                message("Parameters priors and covariates were reset because `by_compartment` was specified.")
                message("(This means that no priors are set and no covariates are used in the returned networkModel.)")
            }
        }
    }
    return(nm)
}

### * set_topo()

### ** Doc

#' Set the topology in a network model.
#'
#' @param nm A \code{networkModel} object (output from
#'     \code{\link{new_networkModel}}).
#' @param ... One or more strings describing the links defining the network
#'     topology. Optionally, links can be given as a data frame. See the
#'     examples for more details about acceptable input formats.
#' @param from Optional, string containing the column name for sources if
#'     links are provided as a data frame.
#' @param to Optional, string containing the column name for destinations if
#'     links are provided as a data frame.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' # A single string can describe several links in one go.
#' m <- new_networkModel() %>%
#'   set_topo("NH4, NO3 -> epi -> pseph, tricor")
#' m
#' topo(m)
#'
#' # Several strings can be given as distinct arguments.
#' m2 <- new_networkModel() %>%
#'   set_topo("NH4, NO3 -> epi -> pseph, tricor",
#'            "NH4 -> FBOM, CBOM", "CBOM <- NO3")
#' m2
#' topo(m2)
#'
#' # Multiple strings can be also be combined into a single argument with `c()`.
#' links <- c("NH4, NO3 -> epi -> pseph, tricor", "NH4 -> FBOM, CBOM",
#'            "CBOM <- NO3")
#' m3 <- new_networkModel() %>%
#'   set_topo(links)
#' m3
#' topo(m3)
#'
#' # A data frame can be used to specify the links.
#' links <- data.frame(source = c("NH4", "NO3", "epi"),
#'                     consumer = c("epi", "epi", "petro"))
#' links
#' m4 <- new_networkModel() %>%
#'   set_topo(links, from = "source", to = "consumer")
#' m4
#' m4$topology[[1]]
#' 
#' @export

### ** Code

set_topo <- function(nm, ..., from = NULL, to = NULL) {
    # Parse the ... argument
    args <- list(...)
    if (length(args) > 1) {
        if (!all(sapply(args, is.character))) {
            stop("Only strings can be used when providing multiple link arguments.\n",
                 "(Maybe you provided both string(s) and data frame(s)?)")
        }
        links <- unlist(args)
    } else {
        links <- args[[1]]
    }
    # Build the topology object
    topology <- make_topology(links = links, from = from, to = to)
    # If the network model is empty, create a row of NULLs
    message_reset <- TRUE
    if (nrow(nm) == 0) {
        nm[1, 1] <- NA
        message_reset <- FALSE
    }
    # Assign all the cells in the "topology" column
    for (i in seq_len(nrow(nm))) {
        nm$topology[[i]] <- topology
    }
    # Add parameter mapping
    nm <- add_param_mapping(nm)
    if (message_reset) {
        msg <- c("`set_topo()` was called on a non-empty networkModel object.",
                 "As a result, the parameter mapping and the priors of the model were reset.")
        message(paste0(msg, collapse = " "))
    }
    return(nm)
}

### * set_steady()

#' Flag some network compartments as being in a steady state
#'
#' @param nm A \code{networkModel} object.
#' @param comps Vector of strings, names of the compartments to set steady.
#' @param which Vector of integers giving the nm rows to update. Default is to
#'     update all rows.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' library(magrittr)
#' x <- new_networkModel() %>%
#'    set_topo("NH4 -> algae -> daphnia") %>%
#'    set_steady("NH4")
#' topo(x)
#'
#' @export

set_steady <- function(nm, comps = NULL, which = NULL) {
    if (is.null(which)) {
        which <- seq_len(nrow(nm))
    }
    stopifnot(all(which %in% seq_len(nrow(nm))))
    # Update row by row
    for (i in which) {
        topo <- nm[["topology"]][[i]]
        steady <- attr(topo, "steadyState")
        for (comp in comps) {
            stopifnot(comp %in% colnames(topo))
            steady <- c(steady, comp)
        }
        attr(nm[["topology"]][[i]], "steadyState") <- steady
    }
    # Return
    return(nm)
}

### * set_split()

#' Flag some network compartments as being split compartments
#'
#' This function automatically adds a default prior (uniform on [0,1]) for the
#' active portion of split compartments.
#' 
#' @param nm A \code{networkModel} object.
#' @param comps Vector of strings, the names of the compartments to set split.
#' @param which Vector of integers giving the nm rows to update. Default is to
#'     update all rows.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' library(magrittr)
#' x <- new_networkModel() %>%
#'    set_topo("NH4 -> algae -> daphnia") %>%
#'    set_split("algae")
#' topo(x)
#'
#' @export

set_split <- function(nm, comps = NULL, which = NULL) {
    if (is.null(which)) {
        which <- seq_len(nrow(nm))
    }
    stopifnot(all(which %in% seq_len(nrow(nm))))
    # Update row by row
    for (i in which) {
        topo <- nm[["topology"]][[i]]
        split <- attr(topo, "split")
        for (comp in comps) {
            stopifnot(comp %in% colnames(topo))
            split <- c(split, comp)
        }
        attr(nm[["topology"]][[i]], "split") <- split
        # Add the corresponding parameters
        params <- nm[["parameters"]][[i]]
        for (comp in comps) {
            paramName <- paste0("portion.act_", comp)
            params <- tibble::add_row(params, in_replicate = paramName,
                                      in_model = paramName)
        }
        nm[["parameters"]][[i]] <- params
    }
    # Update priors
    for (comp in comps) {
        paramName <- paste0("portion.act_", comp)
        attr(nm, "priors") <- tibble::add_row(attr(nm, "priors"),
                                              in_model = paramName,
                                              prior = list(uniform_p(0, 1)))
    }
    return(nm)
}

### * set_half_life()

#' Set the half-life for radioactive tracers
#'
#' Indicating a non-zero value for half-life will add a decay to the marked
#' portion of the tracer element. The decay constant is calculated from the
#' half-life value as:
#'
#' lambda_decay = log(2) / half_life
#'
#' Note that for correct calculations the half-life value should be given in
#' the same time unit (e.g. hour, day) that the time unit used for
#' observations.
#' 
#' @param nm A \code{networkModel} object.
#' @param hl Half-life value, in the same time unit as the observations are (or
#'     will be) given. Setting half-life to zero is equivalent to using a
#'     stable isotope (no decay used in the model).
#' @param quiet Boolean for verbosity.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' library(magrittr)
#' x <- new_networkModel() %>%
#'     set_topo("32P -> root -> leaf") %>%
#'     set_half_life(hl = 14.268)
#' x
#'
#' @export

set_half_life <- function(nm, hl, quiet = FALSE) {
    verbose <- !quiet
    if (hl == 0) {
        if (verbose) {
            message("Half-life set to zero; the model will assume stable isotopes.")
        }
        attr(nm, "lambda_hl") <- NULL
    } else {
        lambda_hl <- log(2) / hl
        if (verbose) {
            message("Half-life set to ", hl, " time units.\n",
                    "(equivalent to a decay rate of ", signif(lambda_hl, 3), " per time unit).")
        }
        attr(nm, "lambda_hl") <- lambda_hl
    }
    return(nm)
}

### * add_pulse_event()

#' Register a pulse event on one of the compartment of a topology
#'
#' When applied to a steady-state compartment, this is equivalent to changing
#' the steady state. Negative values are allowed, so one can add a "pulse" to a
#' steady-state compartment and then later add a similar but negative "pulse"
#' to simulate a drip in a stream for example.
#'
#' @param nm A \code{networkModel} object.
#' @param time Numeric, time at which the pulse is happening.
#' @param comp One compartment name only.
#' @param unmarked Numeric, quantity of unmarked marker added.
#' @param marked Numeric, quantity of marked marker added.
#' @param which Vector of integers giving the nm rows to update. Default is to
#'     update all rows.
#' @param pulses Optionally, a tibble containing the pulse information in
#'     columns. If provided, `comp`, `time`, `unmarked` and `marked` must be
#'     strings giving the corresponding column names.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' m <- trini_mod
#' m$events <- NULL
#' pulses <- tibble::tribble(
#'    ~ stream,    ~ transect, ~ comp, ~ time, ~ qty_14N, ~ qty_15N,
#'        "UL",  "transect.1",  "NH4",     11,         0,  -0.00569,
#'        "UL",  "transect.2",  "NH4",     11,         0,  -0.00264,
#'        "UL",  "transect.3",  "NH4",     11,         0, -0.000726,
#'        "UL",  "transect.1",  "NO3",     11,         0,  -0.00851,
#'        "UL",  "transect.2",  "NO3",     11,         0,  -0.01118,
#'        "UL",  "transect.3",  "NO3",     11,         0,  -0.01244,
#'    )
#' m <- add_pulse_event(m, pulses = pulses, comp = "comp", time = "time",
#'                      unmarked = "qty_14N", marked = "qty_15N")
#' m
#'  
#' 
#' @export

add_pulse_event <- function(nm, time, comp = NULL, unmarked, marked,
                            which = NULL, pulses) {
    if (!missing(pulses)) {
        # Table syntax
        if (is.null(groups(nm))) {
            # No groups
            for (i in seq_len(nrow(pulses))) {
                args <- list(nm = nm,
                             time = pulses[[time]][i],
                             comp = pulses[[comp]][i],
                             unmarked = pulses[[unmarked]][i],
                             marked = pulses[[marked]][i])
                nm <- do.call(add_pulse_event, args)
            }
            return(nm)
        } else {
            # Groups
            grps <- groups(nm)
            gp_vars <- colnames(grps)
            if (!all(gp_vars %in% colnames(pulses))) {
                gp_vars_str <- paste0("\"", gp_vars, "\"")
                stop("The `pulses` argument must have columns specifying the group variables of the network model.\n",
                     "  (Model grouped by: ", paste(gp_vars_str, collapse = ", "), ")\n",
                     "  (Missing column(s) in `pulses` table: ", paste(gp_vars_str[!gp_vars %in% colnames(pulses)], collapse = ", "), ")\n")
            }
            pulses_labels <- apply(as.matrix(pulses[, gp_vars]), 1, paste, collapse = " ")
            for (i in seq_len(nrow(pulses))) {
                grps <- groups(nm)
                grps_labels <- apply(as.matrix(grps), 1, paste, collapse = " ")
                row_i <- which(grps_labels == pulses_labels[i])
                stopifnot(length(row_i) == 1)
                args <- list(nm = nm,
                             time = pulses[[time]][i],
                             comp = pulses[[comp]][i],
                             unmarked = pulses[[unmarked]][i],
                             marked = pulses[[marked]][i],
                             which = row_i)
                nm <- do.call(add_pulse_event, args)
            }
            return(nm)
        }
    }
    # Single pulse
    if (is.null(which)) {
        which <- seq_len(nrow(nm))
    }
    stopifnot(all(which %in% seq_len(nrow(nm))))
    # Create an empty "events" column if needed
    if (!"events" %in% colnames(nm)) {
        nm[["events"]] <- rep(list(NULL), nrow(nm))
    }
    # Update row by row
    for (i in which) {
        topo <- nm[["topology"]][[i]]
        stopifnot(comp %in% colnames(topo))
        this_event <- tibble::tibble(event = "pulse",
                         time = time,
                         compartment = comp,
                         characteristics = list(list(unmarked = unmarked,
                                                marked = marked)))
        nm[["events"]][[i]] <- dplyr::bind_rows(nm[["events"]][[i]],
                                                this_event)
        nm[["events"]][[i]] <- nm[["events"]][[i]][order(nm[["events"]][[i]][["time"]],
                                                         nm[["events"]][[i]][["compartment"]]), ]
    }
    # Return
    return(nm)
}

### * set_init()

#' Set initial conditions in a network model
#'
#' @param nm A \code{networkModel} object (e.g. output from
#'     \code{\link{new_networkModel}})
#' @param data A tibble containing the initial conditions
#' @param comp String, name of the \code{data} column with the compartment names
#' @param size String, name of the \code{data} column with the compartment sizes
#' @param prop String, name of the \code{data} column with the compartment
#'     proportions of marked tracer
#' @param group_by Optional vector of string giving the names of the columns to
#'     use for grouping the data into replicates
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' # Using the topology from the Trinidad case study
#' m <- new_networkModel() %>%
#'   set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph",
#'            "FBOM -> tricor", "petro, tricor -> arg")
#'
#' # Taking initial condtions from the 'lalaja' dataset at t=0
#' inits <- lalaja[lalaja[["time.days"]] == 0, ]
#' inits
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2",
#'               prop = "prop15N", group_by = "transect")
#' m
#' 
#' @export

set_init <- function(nm, data, comp, size, prop, group_by = NULL) {
    # Trim extra compartments
    topo_comps <- unique(unlist(comps(nm)))
    init_comps <- unique(data[[comp]])
    extra_comps <- init_comps[!init_comps %in% topo_comps]
    if (length(extra_comps) > 0) {
        message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the initial data:"),
                       collapse = "\n"), "\n",
                "  - ", paste0(extra_comps, collapse = "\n  - "), "\n")
        data <- data[data[[comp]] %in% topo_comps, ]
    }
    # Build init
    grouped_init <- make_init(data = data, comp = comp,
                              size = size, prop = prop,
                              group_by = group_by)
    out <- merge_nm_by_group(nm = nm, tib = grouped_init,
                             destination = "initial",
                             tib_name = "initial conditions")
    attr(out, "prop_family") <- attr(nm, "prop_family")
    attr(out, "default_columns") <- list(comp = comp,
                                         size = size,
                                         prop = prop,
                                         group_by = group_by)
    # Check that each compartment gets exactly one initial condition
    for (i in seq_len(nrow(out))) {
        z <- out$initial[[i]]
        if (nrow(z) != nrow(na.omit(z))) {
            stop("Some NAs are present in the initial conditions.")
        }
        if (!all(table(z$compartment) == 1)) {
            stop("Some compartments are present twice in the initial conditions data.")
        }
        if (!all(topo_comps %in% z[["compartment"]])) {
            stop("Some compartments are present in the topology but do not have initial conditions.")
        }
    }
    return(out)
}

### * set_obs()

#' Set observations in a network model
#'
#' @param nm A \code{networkModel} object (e.g. output from
#'     \code{\link{new_networkModel}})
#' @param data A tibble containing the observations. If NULL, remove
#'     observations from the model.
#' @param comp String, name of the \code{data} column with the compartment
#'     names
#' @param size String, name of the \code{data} column with the compartment
#'     sizes
#' @param prop String, name of the \code{data} column with the compartment
#'     proportions of heavy tracer
#' @param time String, name of the \code{data} column with the sampling times
#' @param group_by Optional vector of string giving the names of the columns to
#'     use for grouping the data into replicates
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' # Using the topology from the Trinidad case study
#' m <- new_networkModel() %>%
#'   set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph",
#'            "FBOM -> tricor", "petro, tricor -> arg")
#'
#' # Taking initial condtions from the 'lalaja' dataset at t=0
#' inits <- lalaja[lalaja[["time.days"]] == 0, ]
#' inits
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2",
#'               prop = "prop15N", group_by = "transect")
#' m
#'
#' # Taking observations from 'lalaja'
#' m <- set_obs(m, lalaja[lalaja[["time.days"]] > 0, ], time = "time.days")
#' m
#' plot(m)
#' 
#' @export

set_obs <- function(nm, data, comp, size, prop, time, group_by) {
    if (is.null(data)) {
        nm[["observations"]] <- rep(list(NULL), nrow(nm))
        return(nm)
    }
    # Get default columns
    default_columns <- attr(nm, "default_columns")
    columns_from_attr <- c()
    if (missing(comp)) {
        comp <- default_columns[["comp"]]
        if (!is.null(comp)) {
            columns_from_attr <- c(columns_from_attr, "comp")
        }
    }
    if (missing(size)) {
        size <- default_columns[["size"]]
        if (!is.null(size)) {
            columns_from_attr <- c(columns_from_attr, "size")
        }
    }
    if (missing(prop)) {
        prop <- default_columns[["prop"]]
        if (!is.null(prop)) {
            columns_from_attr <- c(columns_from_attr, "prop")
        }
    }
    if (missing(group_by)) {
        group_by <- default_columns[["group_by"]]
        if (!is.null(group_by)) {
            columns_from_attr <- c(columns_from_attr, "group_by")
        }
    }
    if (length(columns_from_attr) > 0) {
        msg <- "Using the same columns by default as the ones used with `set_init()`:\n"
        for (i in columns_from_attr) {
            msg <- c(msg, paste0("  ", i, " = \"", default_columns[[i]], "\"\n"))
        }
        msg <- paste0(msg, collapse = "")
        message(msg)
    }
    # Trim extra compartments
    topo_comps <- unique(unlist(comps(nm)))
    obs_comps <- unique(data[[comp]])
    extra_comps <- obs_comps[!obs_comps %in% topo_comps]
    if (length(extra_comps) > 0) {
        message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the observed data:"),
                       collapse = "\n"), "\n",
                "  - ", paste0(extra_comps, collapse = "\n  - "), "\n")
        data <- data[data[[comp]] %in% topo_comps, ]
    }
    # Build observations
    grouped_obs <- make_obs(data = data, comp = comp, size = size, prop = prop,
                            time = time, group_by = group_by)
    out <- merge_nm_by_group(nm = nm, tib = grouped_obs,
                             destination = "observations",
                             tib_name = "observations")
    default_columns[["time"]] <- time
    attr(out, "default_columns") <- default_columns
    attr(out, "prop_family") <- attr(nm, "prop_family")
    return(out)
}

### * set_params()

#' Set the parameters in a network model
#'
#' @param nm A \code{networkModel} object.
#' @param params A named vector or a tibble with columns c("parameter",
#'     "value") containing the (global) parameter values.
#' @param force Boolean, if FALSE will not overwrite already set parameters.
#' @param quick Boolean, if TRUE take some shortcuts for faster parameter
#'     settings when called by another function. This should usually be left to
#'     the default (FALSE) by a regular package user.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' m <- aquarium_mod
#' p <- sample_params(m)
#' m2 <- set_params(m, p)
#' m2$parameters
#' 
#' @export

set_params <- function(nm, params, force = TRUE, quick = FALSE) {
    if (is.vector(params)) {
        params <- data.frame(value = params, parameter = names(params),
                             stringsAsFactors = FALSE)
    }
    prev_params <- attr(nm, "parameterValues")
    if (!is.null(prev_params) & !force) {
        kept_indices <- !(prev_params$parameter %in% params$parameter)
        kept_params <- prev_params[kept_indices, ]
        new_params <- dplyr::bind_rows(kept_params, params)
    } else {
        new_params <- params
    }
    if (!quick) {
        new_params <- new_params[order(new_params[["parameter"]]), ]
        attr(nm, "parameterValues") <- tibble::as_tibble(new_params)
    } else {
        attr(nm, "parameterValues") <- new_params
    }
    for (i in seq_len(nrow(nm))) {
        nm[["parameters"]][[i]]$value <- new_params[["value"]][match(nm[["parameters"]][[i]]$in_model,
                                                                     new_params[["parameter"]])]
    }
    return(nm)
}

### * set_prior() | set_priors()

#' Set prior(s) for a network model
#'
#' @param x A \code{networkModel} object.
#' @param prior A prior built with e.g. uniform_p() or hcauchy_p(). Call
#'     \code{available_priors()} to see a table of implemented
#'     priors. Alternatively, if \code{prior} is a tibble, the function will
#'     try to use it to set parameter priors. The format of such an argument is
#'     the same as the format of the output of the getter function
#'     \code{priors()} (see examples). Note that if `prior` is given as a
#'     tibble, all other arguments (except `x`) are disregarded.
#' @param param String, target parameter or regexp to target several
#'     parameters. Default is the empty string \code{""}, which will match all
#'     parameters.
#' @param use_regexp Boolean, if \code{TRUE} (the default) then \code{param} is
#'     used as a regular expression to match one or several parameter names.
#' @param quiet Boolean, if \code{FALSE} print a message indicating which
#'     parameters had their prior modified.
#'
#' @return A \code{networkModel} object.
#'
#' @examples
#' # Copy `aquarium_mod`
#' m <- aquarium_mod
#' priors(m)
#'
#' # Modify the priors of `m`
#' m <- set_priors(m, exponential_p(0.5), "lambda")
#' priors(m)
#'
#' # Re-apply priors from the original `aquarium_mod`
#' prev_priors <- priors(aquarium_mod)
#' prev_priors
#' m <- set_priors(m, prev_priors)
#' priors(m)
#' 
#' @export

set_prior <- function(x, prior, param = "", use_regexp = TRUE, quiet = FALSE) {
    verbose <- !quiet
    params <- params(x, simplify = TRUE)
    priors <- priors(x)
    stopifnot(setequal(params, priors[["in_model"]]))
    params <- priors[["in_model"]]
    # If `prior` is a tibble
    if (is(prior, "tbl_df")) {
        if (!valid_prior_tbl(prior)) {
            stop("`prior` is not a valid prior tibble.")
        }
        prior <- prior[, c("in_model", "prior")]
        extra_params <- which(!prior[["in_model"]] %in% params)
        if (length(extra_params) > 0) {
            message("Some parameter names in `prior` are not used in the network model and will be ignored.\n",
                    paste0(paste0("\"", prior[["in_model"]][extra_params], "\""), collapse = ", "))
        }
        prior <- prior[which(prior[["in_model"]] %in% params), ]
        param_indices <- match(prior[["in_model"]], params)
        for (i in seq_along(param_indices)) {
            priors[["prior"]][[param_indices[i]]] <- prior[["prior"]][[i]]
        }
        if (verbose) {
            message("Prior modified for parameter(s): \n  - ",
                    paste0(prior[["in_model"]], collapse = "\n  - "))
        }
        attr(x, "priors") <- priors
        return(x)
    }
    # If `prior` is not a tibble
    if (!is(prior, "prior")) {
        stop("`prior` must be a valid prior. See `available_priors()`.")
    }
    if (use_regexp) {
        param_indices <- which(grepl(pattern = param, x = params))
    } else {
        param_indices <- which(params == param)
    }
    if (length(param_indices) == 0) {
        stop("No matching parameter found for: ", param, ".\n",
             "Available parameters are: \n- ",
             paste0(params, collapse = "\n- "))
    }
    for (i in param_indices) {
        priors[["prior"]][[i]] <- prior
    }
    if (verbose) {
        message("Prior modified for parameter(s): \n  - ",
                paste0(params[param_indices], collapse = "\n  - "))
    }
    attr(x, "priors") <- priors
    return(x)
}

#' @rdname set_prior
#' @export

set_priors <- set_prior
    
### * add_covariates()

#' Add fixed effects of one or several covariates to some parameters.
#'
#' Note that new global parameters are not given any default prior.
#'
#' @param nm A \code{networkModel} object.
#' @param ... One or several formulas defining the covariates.
#' @param use_regexpr Boolean, use regular expression to match the parameters
#'     affected by the formulas?
#' 
#' @return A \code{networkModel} object.
#'
#' @examples
#' # Using a subset of the topology from the Trinidad case study
#' m <- new_networkModel() %>%
#'   set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph")
#'
#' # Taking initial condtions from the 'lalaja' dataset at t=0
#' # Grouping by transect id
#' inits <- lalaja[lalaja[["time.days"]] == 0, ]
#' inits
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2",
#'               prop = "prop15N", group_by = "transect")
#' m
#'
#' # Default model
#' params(m, simplify = TRUE)
#'
#' # Adding an effect of the "transect" covariate on some parameters
#' m <- add_covariates(m, upsilon_epi_to_pseph ~ transect)
#' params(m, simplify = TRUE)
#'
#' @export

add_covariates <- function(nm, ..., use_regexpr = TRUE) {
    formula <- list(...)
    # Apply formula
    for (eachFormula in formula) {
        nm <- refresh_param_mapping(nm, eachFormula, use_regexpr = use_regexpr)
    }
    # Refresh priors
    current_priors <- priors(nm)
    global_params <- params(nm, simplify = TRUE)
    default_priors <- tibble::tibble(in_model = global_params)
    default_priors$prior <- rep(list(NULL), nrow(default_priors))
    kept_priors <- current_priors[current_priors$in_model %in% global_params, ]
    new_priors <- default_priors[!default_priors$in_model %in%
                                 current_priors$in_model, ]
    priors <- dplyr::bind_rows(kept_priors, new_priors)
    priors <- priors[order(priors$in_model), ]
    # Return
    attr(nm, "priors") <- priors
    return(nm)
}

### * print.networkModel()

#' Print method for \code{networkModel} objects
#'
#' @param x A \code{networkModel} object.
#' @param ... Passsed to the next method.
#'
#' @return Called for the side effect of printing a network model object.
#' 
#' @method print networkModel
#'
#' @export
#' 

print.networkModel <- function(x, ...) {
    NextMethod()
    lambda_hl <- attr(x, "lambda_hl")
    if (!is.null(lambda_hl)) {
        hl <- signif(log(2) / lambda_hl, 5)
        cat("# Half-life of marked tracer:", hl, "time units.\n")
    }
}

Try the isotracer package in your browser

Any scripts or data that you put into this service are public.

isotracer documentation built on Sept. 22, 2023, 1:07 a.m.