
#' @title Epidemic Parameters for Stochastic Network Models
#'
#' @description Sets the epidemic parameters for stochastic network models
#'              simulated with [netsim()].
#'
#' @param inf.prob Probability of infection per transmissible act between
#'        a susceptible and an infected person. In two-group models, this is the
#'        probability of infection to the group 1 nodes. This may also be a
#'        vector of probabilities, with each element corresponding to the
#'        probability in that time step of infection (see Time-Varying
#'        Parameters below).
#' @param inter.eff Efficacy of an intervention which affects the per-act
#'        probability of infection. Efficacy is defined as 1 - the relative
#'        hazard of infection given exposure to the intervention, compared to no
#'        exposure.
#' @param inter.start Time step at which the intervention starts, between 1 and
#'        the number of time steps specified in the model. This will default to
#'        1 if `inter.eff` is defined but this parameter is not.
#' @param act.rate Average number of transmissible acts *per partnership*
#'        per unit time (see `act.rate` Parameter below). This may also be
#'        a vector of rates, with each element corresponding to the rate in
#'        that time step of infection (see Time-Varying Parameters below).
#' @param rec.rate Average rate of recovery with immunity (in `SIR` models)
#'        or re-susceptibility (in `SIS` models). The recovery rate is the
#'        reciprocal of the disease duration. For two-group models, this is the
#'        recovery rate for group 1 persons only. This parameter is only used
#'        for `SIR` and `SIS` models. This may also be a vector
#'        of rates, with each element corresponding to the rate in that time
#'        step of infection (see Time-Varying Parameters below).
#' @param a.rate Arrival or entry rate. For one-group models, the arrival rate
#'        is the rate of new arrivals per person per unit time. For two-group
#'        models, the arrival rate is parameterized as a rate per group 1
#'        person per unit time, with the `a.rate.g2` rate set as described
#'        below.
#' @param ds.rate Departure or exit rate for susceptible persons. For two-group
#'        models, it is the rate for group 1 susceptible persons only.
#' @param di.rate Departure or exit rate for infected persons. For two-group
#'        models, it is the rate for group 1 infected persons only.
#' @param dr.rate Departure or exit rate for recovered persons. For two-group
#'        models, it is the rate for group 1 recovered persons only. This
#'        parameter is only used for `SIR` models.
#' @param inf.prob.g2 Probability of transmission given a transmissible act
#'        between a susceptible group 2 person and an infected group 1 person.
#'        It is the probability of transmission to group 2 members.
#' @param rec.rate.g2 Average rate of recovery with immunity (in `SIR`
#'        models) or re-susceptibility (in `SIS` models) for group 2
#'        persons. This parameter is only used for two-group `SIR` and
#'        `SIS` models.
#' @param a.rate.g2 Arrival or entry rate for group 2. This may either be
#'        specified numerically as the rate of new arrivals per group 2 person
#'        per unit time, or as `NA`, in which case the group 1 rate,
#'        `a.rate`, governs the group 2 rate. The latter is used when, for
#'        example, the first group is conceptualized as female, and the female
#'        population size determines the arrival rate. Such arrivals are evenly
#'        allocated between the two groups.
#' @param ds.rate.g2 Departure or exit rate for group 2 susceptible persons.
#' @param di.rate.g2 Departure or exit rate for group 2 infected persons.
#' @param dr.rate.g2 Departure or exit rate for group 2 recovered persons. This
#'        parameter is only used for `SIR` model types.
#'
#' @param ... Additional arguments passed to model.
#'
#' @details
#' `param.net` sets the epidemic parameters for the stochastic network
#' models simulated with the [netsim()] function. Models
#' may use the base types, for which these parameters are used, or new process
#' modules which may use these parameters (but not necessarily). A detailed
#' description of network model parameterization for base models is found in
#' the [Network Modeling for Epidemics](https://epimodel.github.io/sismid/)
#' tutorials.
#'
#' For base models, the model specification will be chosen as a result of
#' the model parameters entered here and the control settings in
#' [control.net()]. One-group and two-group models are available,
#' where the latter assumes a heterogeneous mixing between two distinct
#' partitions in the population (e.g., men and women). Specifying any two-group
#' parameters (those with a `.g2`) implies the simulation of a two-group
#' model. All the parameters for a desired model type must be specified, even if
#' they are zero.
#'
#' @section The `act.rate` Parameter:
#' A key difference between these network models and DCM/ICM classes is the
#' treatment of transmission events. With DCM and ICM, contacts or partnerships
#' are mathematically instantaneous events: they have no duration in time, and
#' thus no changes may occur within them over time. In contrast, network models
#' allow for partnership durations defined by the dynamic network model,
#' summarized in the model dissolution coefficients calculated in
#' [dissolution_coefs()]. Therefore, the `act.rate` parameter has
#' a different interpretation here, where it is the number of transmissible acts
#' *per partnership* per unit time.
#'
#' @section Time-Varying Parameters:
#' The `inf.prob`, `act.rate`, `rec.rate` arguments (and their
#' `.g2` companions) may be specified as time-varying parameters by passing
#' in a vector of probabilities or rates, respectively. The value in each
#' position on the vector then corresponds to the probability or rate at that
#' discrete time step for the infected partner. For example, an `inf.prob`
#' of `c(0.5, 0.5, 0.1)` would simulate a 0.5 transmission probability for
#' the first two time steps of a person's infection, followed by a 0.1 for the
#' third time step. If the infected person has not recovered or exited the
#' population by the fourth time step, the third element in the vector will
#' carry forward until one of those events occurs or the simulation ends. For
#' further examples, see the
#' [Network Modeling for Epidemics](https://epimodel.github.io/sismid/) tutorials.
#'
#' @section Random Parameters:
#' In addition to deterministic parameters in either fixed or time-varying
#' varieties above, one may also include a generator for random parameters.
#' These might include a vector of potential parameter values or a statistical
#' distribution definition; in either case, one draw from the generator would
#' be completed per individual simulation. This is possible by passing a list
#' named `random.params` into `param.net`, with each element of
#' `random.params` a named generator function. See the help page and
#' examples in [generate_random_params()]. A simple factory function
#' for sampling is provided with [param_random()] but any function
#' will do.
#'
#' @section Using a Parameter data.frame:
#' It is possible to set input parameters using a specifically formatted
#' `data.frame` object. The first 3 columns of this `data.frame` must
#' be:
#'
#'  * `param`: The name of the parameter. If this is a non-scalar
#'    parameter (a vector of length > 1), end the parameter name with the
#'    position on the vector (e.g., `"p_1"`, `"p_2"`, ...).
#'  * `value`: the value for the parameter (or the value of the
#'    parameter in the Nth position if non-scalar).
#'  * `type`: a character string containing either `"numeric"`,
#'    `"logical"`, or `"character"` to define the parameter object
#'    class.
#'
#'
#' In addition to these 3 columns, the `data.frame` can contain any number
#' of other columns, such as `details` or `source` columns to document
#' parameter meta-data. However, these extra columns will not be used by
#' EpiModel.
#'
#' This data.frame is then passed in to `param.net` under a
#' `data.frame.parameters` argument. Further details and examples are
#' provided in the "Working with Model Parameters in EpiModel" vignette.
#'
#' @section Parameters with New Modules:
#' To build original models outside of the base models, new process modules
#' may be constructed to replace the existing modules or to supplement the
#' existing set. These are passed into the control settings in
#' [control.net()]. New modules may use either the existing model
#' parameters named here, an original set of parameters, or a combination of
#' both. The `...` allows the user to pass an arbitrary set of original
#' model parameters into `param.net`. Whereas there are strict checks with
#' default modules for parameter validity, this becomes a user
#' responsibility when using new modules.
#'
#' @return An `EpiModel` object of class `param.net`.
#'
#' @seealso Use [init.net()] to specify the initial conditions and
#'          [control.net()] to specify the control settings. Run the
#'          parameterized model with [netsim()].
#'
#' @keywords parameterization
#'
#' @export
#'
#' @examples
#' \donttest{
#' ## Example SIR model parameterization with fixed and random parameters
#' # Network model estimation
#' nw <- network_initialize(n = 100)
#' formation <- ~edges
#' target.stats <- 50
#' coef.diss <- dissolution_coefs(dissolution = ~offset(edges), duration = 20)
#' est <- netest(nw, formation, target.stats, coef.diss, verbose = FALSE)
#'
#' # Random epidemic parameter list (here act.rate values are sampled uniformly
#' # with helper function param_random, and inf.prob follows a general Beta
#' # distribution with the parameters shown below)
#' my_randoms <- list(
#'   act.rate = param_random(1:3),
#'   inf.prob = function() rbeta(1, 1, 2)
#' )
#'
#' # Parameters, initial conditions, and control settings
#' param <- param.net(rec.rate = 0.02, random.params = my_randoms)
#'
#' # Printing parameters shows both fixed and and random parameter functions
#' param
#'
#' # Set initial conditions and controls
#' init <- init.net(i.num = 10, r.num = 0)
#' control <- control.net(type = "SIR", nsteps = 10, nsims = 3, verbose = FALSE)
#'
#' # Simulate the model
#' sim <- netsim(est, param, init, control)
#'
#' # Printing the sim object shows the randomly drawn values for each simulation
#' sim
#'
#' # Parameter sets can be extracted with:
#' get_param_set(sim)
#' }
#'
param.net <- function(inf.prob, inter.eff, inter.start, act.rate, rec.rate,
                      a.rate, ds.rate, di.rate, dr.rate, inf.prob.g2,
                      rec.rate.g2, a.rate.g2, ds.rate.g2, di.rate.g2,
                      dr.rate.g2, ...) {


  # Get arguments
  dot.args <- list(...)
  names.dot.args <- names(dot.args)

  # Use "data.frame.params" as default if available
  if ("data.frame.params" %in% names.dot.args) {
    p <- param.net_from_table(dot.args[["data.frame.params"]])
    dot.args[["data.frame.params"]] <- NULL
    names.dot.args <- names(dot.args)
  } else {
    p <- list()
  }

  formal.args <- formals(sys.function())
  formal.args[["..."]] <- NULL
  for (arg in names(formal.args)) {
    if (as.logical(mget(arg) != "")) {
      p[arg] <- list(get(arg))
    }
  }
  if (length(dot.args) > 0) {
    for (i in seq_along(dot.args)) {
      p[[names.dot.args[i]]] <- dot.args[[i]]
    }
  }

  ## random.params checks
  if ("random.params" %in% names.dot.args) {
    for (nm in names(p[["random.params"]])) {
      if (nm %in% names(p)) {
        warning(
          "The parameter `", nm, "` is defined twice, once as fixed",
          " and once as a random parameter.\n Only the random parameter",
          " definition will be used."
        )
      }
    }
  }

  ## Defaults and Checks
  # Check for old .m2 parameter suffix
  m2.flag <- grep(".m2", names(p))
  if (length(m2.flag) > 0) {
    stop("Parameters using the .m2 suffix have been removed. ",
         "Use the .g2 suffix instead (e.g., inf.prob.g2).")
  }
  if (missing(act.rate)) {
    p[["act.rate"]] <- 1
  }
  p[["vital"]] <- ifelse(!missing(a.rate) | !missing(ds.rate) |
                           !missing(di.rate) | !missing(dr.rate), TRUE, FALSE)
  if ("act.rate.g2" %in% names.dot.args) {
    warning("act.rate.g2 parameter was entered. ",
            "If using built-in models, only act.rate parameter will apply.")
  }

  if (!is.null(p[["inter.eff"]]) && is.null(p[["inter.start"]])) {
    p[["inter.start"]] <- 1
  }

  ## Output
  class(p) <- c("param.net", "list")
  return(p)
}

#' @title Update Model Parameters for Stochastic Network Models
#'
#' @description Updates epidemic model parameters originally set with
#'              [param.net()] and adds new parameters.
#'
#' @param param Object of class `param.net`, output from function of same
#'              name.
#' @param new.param.list Named list of new parameters to add to original
#'        parameters.
#'
#' @details
#' This function can update any original parameters specified with
#' [param.net()] and add new parameters. This function would be used
#' if the inputs to [param.net()] were a long list of fixed model
#' parameters that needed supplemental replacements or additions for particular
#' model runs (e.g., changing an intervention efficacy parameter but leaving all
#' other parameters fixed).
#'
#' The `new.param.list` object should be a named list object containing
#' named parameters matching those already in `x` (in which case those
#' original parameter values will be replaced) or not matching (in which case
#' new parameters will be added to `param`).
#'
#' `update_params` modifies a `param.net` object **before** it is passed
#' to [netsim()]; it is the recommended helper for tweaking parameters
#' outside of a running simulation. To modify parameters from *inside* a
#' module function during a simulation, use [set_param()] on the live
#' `dat` object instead. There is no analogous `update_controls()` or
#' `update_inits()` helper: for a `control.net` or `init.net` object,
#' edit the list directly (e.g., `ctrl$nsteps <- 1000`) or rebuild it
#' with a fresh call to the constructor. For *scheduled* mid-simulation
#' changes to parameters or control settings, see the
#' `.param.updater.list` argument to [param.net()] and the
#' `.control.updater.list` argument to [control.net()].
#'
#' @return
#' An updated list object of class `param.net`, which can be passed to the
#' EpiModel function [netsim()].
#'
#' @seealso [set_param()] for editing parameters inside a module function
#'   during a simulation; [param.net()] for the parameter constructor;
#'   [netsim()] for running a simulation.
#'
#' @examples
#' x <- param.net(inf.prob = 0.5, act.rate = 2)
#' y <- list(inf.prob = 0.75, dx.rate = 0.2)
#' z <- update_params(x, y)
#' print(z)
#'
#' @export
#'
update_params <- function(param, new.param.list) {

  if (!inherits(param, "param.net")) {
    stop("x should be object of class param.net")
  }
  if (!inherits(new.param.list, "list")) {
    stop("new.param.list should be object of class list")
  }

  for (ii in seq_along(new.param.list)) {
    param[[names(new.param.list)[ii]]] <- new.param.list[[ii]]
  }

  return(param)
}


#' @title Create a Value Sampler for Random Parameters
#'
#' @description This function returns a 0 argument function that can be used as
#'   a generator function in the `random.params` argument of the
#'   [param.net()] function.
#'
#' @param values A vector of values to sample from.
#' @param prob A vector of weights to use during sampling. If `NULL`,
#'        all values have the same probability of being picked
#'        (default = `NULL`).
#'
#' @return A 0 argument generator function to sample one of the values from the
#' `values` vector.
#'
#' @seealso [param.net()] and [generate_random_params()]
#' @export
#'
#' @examples
#' # Define function with equal sampling probability
#' a <- param_random(1:5)
#' a()
#'
#' # Define function with unequal sampling probability
#' b <- param_random(1:5, prob = c(0.1, 0.1, 0.1, 0.1, 0.6))
#' b()
#'
param_random <- function(values, prob = NULL) {
  if (!is.null(prob) && length(prob) != length(values)) {
    stop("incorrect number of probabilites")
  }

  f <- function() {
    return(sample(x = values, size = 1, prob = prob, replace = TRUE))
  }

  return(f)
}


#' @title Generate Values for Random Parameters
#'
#' @description This function uses the generative functions in the
#'              `random.params` list to create values for the parameters.
#'
#' @param param The `param` argument received by the `netsim`
#'              functions.
#' @param verbose Should the function output the generated values
#'                (default = FALSE)?
#'
#' @return A fully instantiated `param` list.
#'

#' @section `random.params`:
#' The `random.params` argument to the [param.net()] function
#' must be a named list of functions that each return a value that can be used
#' as the argument with the same name. In the example below, `param_random`
#' is a function factory provided by EpiModel for `act.rate` and
#' for `tx.halt.part.prob` we provide bespoke functions. A function factory
#' is a function that returns a new function
#' (see https://adv-r.hadley.nz/function-factories.html).
#'
#' @section Generator Functions:
#' The functions used inside `random_params` must be 0 argument functions
#' returning a valid value for the parameter with the same name.
#'
#' @section `param_random_set`:
#' The `random_params` list can optionally contain a
#' `param_random_set` element. It must be a `data.frame` of possible
#' values to be used as parameters.
#'
#' The column names must correspond either to:
#' the name of one parameter, if this parameter is of size 1; or the name of one
#' parameter with "_1", "_2", etc. appended, with the number representing the
#' position of the value, if this parameter is of size > 1. This means that the
#' parameter names cannot contain any underscores "_" if you intend to use
#' `param_random_set`.
#'
#' The point of the `param.random.set` `data.frame` is to allow the
#' random parameters to be correlated. To achieve this, a whole row of the
#' `data.frame` is selected for each simulation.
#'
#' @export
#'
#' @examples
#' \dontrun{
#'
#' ## Example with only the generator function
#'
#' # Define random parameter list
#' my_randoms <- list(
#'   act.rate = param_random(c(0.25, 0.5, 0.75)),
#'   tx.prob = function() rbeta(1, 1, 2),
#'   stratified.test.rate = function() c(
#'     rnorm(1, 0.05, 0.01),
#'     rnorm(1, 0.15, 0.03),
#'     rnorm(1, 0.25, 0.05)
#'   )
#' )
#'
#' # Parameter model with fixed and random parameters
#' param <- param.net(inf.prob = 0.3, random.params = my_randoms)
#'
#' # Below, `tx.prob` is set first to 0.3 then assigned a random value using
#' # the function from `my_randoms`. A warning notifying of this overwrite is
#' # therefore produced.
#' param <- param.net(tx.prob = 0.3, random.params = my_randoms)
#'
#'
#' # Parameters are drawn automatically in netsim by calling the function
#' # within netsim_loop. Demonstrating draws here but this is not used by
#' # end user.
#' paramDraw <- generate_random_params(param, verbose = TRUE)
#' paramDraw
#'
#'
#' ## Addition of the `param.random.set` `data.frame`
#'
#' # This function will generate sets of correlated parameters
#'  generate_correlated_params <- function() {
#'    param.unique <- runif(1)
#'    param.set.1 <- param.unique + runif(2)
#'    param.set.2 <- param.unique * rnorm(3)
#'
#'    return(list(param.unique, param.set.1, param.set.2))
#'  }
#'
#'  # Data.frame set of random parameters :
#'  correlated_params <- t(replicate(10, unlist(generate_correlated_params())))
#'  correlated_params <- as.data.frame(correlated_params)
#'  colnames(correlated_params) <- c(
#'    "param.unique",
#'    "param.set.1_1", "param.set.1_2",
#'    "param.set.2_1", "param.set.2_2", "param.set.2_3"
#'  )
#'
#' # Define random parameter list with the `param.random.set` element
#' my_randoms <- list(
#'   act.rate = param_random(c(0.25, 0.5, 0.75)),
#'   param.random.set = correlated_params
#' )
#'
#' # Parameter model with fixed and random parameters
#' param <- param.net(inf.prob = 0.3, random.params = my_randoms)
#'
#' # Parameters are drawn automatically in netsim by calling the function
#' # within netsim_loop. Demonstrating draws here but this is not used by
#' # end user.
#' paramDraw <- generate_random_params(param, verbose = TRUE)
#' paramDraw
#'
#' }
generate_random_params <- function(param, verbose = FALSE) {
  if (is.null(param[["random.params"]]) ||
        length(param[["random.params"]]) == 0) {
    return(param)
  } else {
    random.params <- param[["random.params"]]
  }

  if (!is.list(random.params)) {
    stop("`random.params` must be named list of functions")
  }

  rng_names <- names(random.params)
  if (any(rng_names == "")) {
    stop("all elements of `random.params` must be named")
  }

  rng_values <- list()

  if ("param.random.set" %in% rng_names) {
    # Take `param.random.set` out of the `random.params` list
    param.random.set <- random.params[["param.random.set"]]
    random.params[["param.random.set"]] <- NULL
    rng_names <- names(random.params)

    if (!is.data.frame(param.random.set)) {
      stop("`param.random.set` must be a data.frame")
    }

    # Pick one row of the `data.frame`
    sampled.row <- sample.int(nrow(param.random.set), 1)

    # Convert to `param` format
    sampled.set <- unflatten_params(param.random.set[sampled.row, ])

    # Update `rng_values`
    rng_values <- update_list(rng_values, sampled.set)
  }

  if (!all(vapply(random.params, is.function, TRUE))) {
    stop("all elements of `random.params` must be functions \n",
         "(Except 'param.random.set')")
  }

  duplicated.rng <- names(rng_values) %in% rng_names
  if (any(duplicated.rng)) {
    warning("Some parameters are set to be randomly assigned twice: \n",
            paste0(rng_names[duplicated.rng], collapse = ", "), "\n\n",
            "The version from a generator function will be used")
  }

  rng_values[rng_names] <- lapply(random.params, do.call, args = list())
  param <- update_list(param, rng_values)

  param[["random.params.values"]] <- rng_values

  if (verbose == TRUE) {
    msg <-
      "The following values were randomly generated for the given parameters: \n"
    msg <- c(msg, paste0("`", names(rng_values), "`: ", rng_values, "\n"))
    message(msg)
  }

  return(param)
}


#' @title Initial Conditions for Stochastic Network Models
#'
#' @description Sets the initial conditions for stochastic network models
#'              simulated with `netsim`.
#'
#' @param i.num Number of initial infected persons. For two-group models, this
#'        is the number of initial group 1 infected persons.
#' @param r.num Number of initial recovered persons. For two-group models, this
#'        is the number of initial group 1 recovered persons. This parameter is
#'        only used for the `SIR` model type.
#' @param i.num.g2 Number of initial infected persons in group 2. This parameter
#'        is only used for two-group models.
#' @param r.num.g2 Number of initial recovered persons in group 2. This
#'        parameter is only used for two-group `SIR` models.
#' @param status.vector A vector of length equal to the size of the input
#'        network, containing the status of each node. Setting status here
#'        overrides any inputs passed in the `.num` arguments.
#' @param infTime.vector A vector of length equal to the size of the input
#'        network, containing the (historical) time of infection for each of
#'        those nodes with a current status of `"i"`. Can only be used if
#'        `status.vector` is used, and must contain `NA` values for
#'        any nodes whose status is not `"i"`.
#' @param ... Additional initial conditions passed to model.
#'
#' @details
#' The initial conditions for a model solved with [netsim()] should be
#' input into the `init.net` function. This function handles initial
#' conditions for both base models and new modules. For an overview of
#' specifying initial conditions across a variety of base network models,
#' consult the [Network Modeling for Epidemics](https://epimodel.github.io/sismid/)
#' tutorials.
#'
#' @return An `EpiModel` object of class `init.net`.
#'
#' @seealso Use [param.net()] to specify model parameters and
#'          [control.net()] to specify the control settings. Run the
#'          parameterized model with [netsim()].
#'
#' @keywords parameterization
#'
#' @export
#'
#' @examples
#' # Example of using status.vector and infTime.vector together
#' n <- 100
#' status <- sample(c("s", "i"), size = n, replace = TRUE, prob = c(0.8, 0.2))
#' infTime <- rep(NA, n)
#' infTime[which(status == "i")] <- -rgeom(sum(status == "i"), prob = 0.01) + 2
#'
#' init.net(status.vector = status, infTime.vector = infTime)
#'
init.net <- function(i.num, r.num, i.num.g2, r.num.g2,
                     status.vector, infTime.vector, ...) {

  # Get arguments
  p <- list()
  formal.args <- formals(sys.function())
  formal.args[["..."]] <- NULL
  for (arg in names(formal.args)) {
    if (as.logical(mget(arg) != "")) {
      p[arg] <- list(get(arg))
    }
  }
  dot.args <- list(...)
  names.dot.args <- names(dot.args)
  if (length(dot.args) > 0) {
    for (i in seq_along(dot.args)) {
      p[[names.dot.args[i]]] <- dot.args[[i]]
    }
  }

  ## Defaults and checks

  # Check for old .m2 initial condition suffix
  m2.init <- grep("m2", names(p), value = TRUE)
  if (length(m2.init) > 0) {
    stop("Initial conditions using the .m2 suffix have been removed. ",
         "Use the .g2 suffix instead (e.g., i.num.g2).")
  }
  if (!is.null(p[["i.num"]]) && !is.null(p[["status.vector"]])) {
    stop("Use i.num OR status.vector to set initial infected")
  }
  if (!is.null(p[["infTime.vector"]]) && is.null(p[["status.vector"]])) {
    stop("infTime.vector may only be used if status.vector is used")
  }
  if (!is.null(p[["infTime.vector"]]) &&
        length(p[["infTime.vector"]]) != length(p[["status.vector"]])) {
    stop("Length of infTime.vector must match length of status.vector")
  }


  ## Output
  class(p) <- c("init.net", "list")
  return(p)
}


#' @title Control Settings for Stochastic Network Models
#'
#' @description Sets the controls for stochastic network models simulated with
#'              [`netsim`].
#'
#' @param type Disease type to be modeled, with the choice of `"SI"` for Susceptible-Infected
#'        diseases, `"SIR"` for Susceptible-Infected-Recovered diseases, and `"SIS"` for
#'        Susceptible-Infected-Susceptible diseases.
#' @param nsteps Number of time steps to simulate the model over. This must be a positive integer
#'        that is equal to the final step of a simulation. If a simulation is restarted with `start`
#'        argument, this number must be at least one greater than that argument's value.
#' @param nsims The total number of disease simulations.
#' @param ncores Number of processor cores to run multiple simulations on, using
#'        the `future` framework with `multisession`. See `future.use.plan` for
#'        other `future` backends.
#' @param start For models with network resimulation, time point to start up the simulation. For
#'        restarted simulations, this must be one greater than the final time step in the prior
#'        simulation and must be less than the value in `nsteps`.
#' @param resimulate.network If `TRUE`, resimulate the network at each time step. This is required
#'        when the epidemic or demographic processes impact the network structure (e.g., vital
#'        dynamics). This parameter controls whether `resim_nets.FUN` performs actual network
#'        resimulation; it does not affect the order in which modules are executed (see
#'        `module.order`). Setting `tergmLite = TRUE` forces `resimulate.network = TRUE` with a
#'        warning.
#' @param tergmLite Logical indicating usage of either `tergm` (`tergmLite = FALSE`), or `tergmLite`
#'        (`tergmLite = TRUE`). Default of `FALSE`. When `TRUE`, `resimulate.network` is
#'        automatically set to `TRUE` (with a warning if the user explicitly set it to `FALSE`).
#' @param cumulative.edgelist If `TRUE`, calculates a cumulative edgelist within the network
#'        simulation module. This is used when tergmLite is used and the entire networkDynamic
#'        object is not used.
#' @param truncate.el.cuml Number of time steps of the cumulative edgelist to retain. See help for
#'        [`update_cumulative_edgelist`] for options.
#' @param attr.rules A list containing the  rules for setting the attributes of incoming nodes, with
#'        one list element per attribute to be set (see details below).
#' @param epi.by A character vector of length 1 containing a nodal attribute for which subgroup
#'        stratified prevalence summary statistics are calculated. This nodal attribute must be
#'        contained in the network model formation formula, otherwise it is ignored.
#' @param initialize.FUN Module to initialize the model at time 1, with the default function of
#'        [`initialize.net`].
#' @param departures.FUN Module to simulate departure or exit, with the default function of
#'        [`departures.net`].
#' @param arrivals.FUN Module to simulate arrivals or entries, with the default function of
#'        [`arrivals.net`].
#' @param recovery.FUN Module to simulate disease recovery, with the default function of
#'        [`recovery.net`].
#' @param resim_nets.FUN Module to resimulate the network at each time step, with the default
#'        function of [`resim_nets`].
#' @param summary_nets.FUN Module to extract summary statistics of the network
#'        at each time step, with the default function of [`summary_nets`].
#' @param infection.FUN Module to simulate disease infection, with the default function of
#'        [`infection.net`].
#' @param nwupdate.FUN Module to handle updating of network structure and nodal attributes due to
#'        exogenous epidemic model processes, with the default function of [`nwupdate.net`].
#' @param prevalence.FUN Module to calculate disease prevalence at each time step, with the default
#'        function of [`prevalence.net`].
#' @param verbose.FUN Module to print simulation progress to screen, with the default function of
#'        [`verbose.net`].
#' @param module.order A character vector of module names that lists modules in the order in which
#'        they should be evaluated within each time step. If `NULL`, the modules will be evaluated
#'        as follows: first any new modules supplied through `...` in the order in which they are
#'        listed, then the built-in modules in the order in which they are listed as arguments
#'        above. `initialize.FUN` will always be run first and `verbose.FUN` will always be run last.
#'        Module ordering is independent of `resimulate.network`: the specified order is always
#'        respected regardless of whether network resimulation is enabled. In the default ordering,
#'        `resim_nets.FUN` runs before `infection.FUN`, so the network is resimulated before
#'        transmission is evaluated at each time step.
#' @param save.nwstats If `TRUE`, save network statistics in a data frame. The statistics to be
#'        saved are specified in the `nwstats.formula` argument.
#' @param nwstats.formula A right-hand sided ERGM formula that includes network statistics of
#'        interest, with the default to the formation formula terms. Supports [`multilayer`]
#'        specification.
#' @param save.network If `TRUE`, networkDynamic or networkLite object is saved at simulation end.
#' @param save.transmat If `TRUE`, complete transmission matrix is saved at simulation end.
#' @param save.run If `TRUE`, the `run` sublist of `dat` is saved, allowing a
#'   simulation to restart from this output.
#' @param save.cumulative.edgelist If `TRUE`, the `cumulative.edgelist` is saved at simulation end.
#' @param save.other A character vector of elements on the `netsim_dat` main data list to save out
#'        after each simulation. One example for base models is the attribute list, `"attr"`, at
#'        the final time step.
#' @param verbose If `TRUE`, print model progress to the console.
#' @param verbose.int Time step interval for printing progress to console, where `0` prints
#'        completion status of entire simulation and positive integer `x` prints progress after
#'        every `x` time steps. The default is to print progress after each time step.
#' @param skip.check If `TRUE`, skips the default error checking for the structure and consistency
#'        of the parameter values, initial conditions, and control settings before running base
#'        epidemic models. Setting this to `FALSE` is recommended when running models with new
#'        modules specified.
#' @param raw.output If `TRUE`, `netsim` will output a list of raw data (one per simulation) instead
#'        of a cleaned and formatted `netsim` object.
#' @param future.use.plan If `FALSE`, `netsim` will use `multisession` is used with `workers = ncores for its
#'        parallelization. If `TRUE`, `netsim` will use the user defined plan from `globalEnv`. Finally, it can
#'        take the output of a `future::tweak()` call to setup a user defined temporary plan within `netsim`.
#'        Which can be useful for distributed computation (HPC).
#' @param tergmLite.track.duration If `TRUE`, track duration information for models in `tergmLite`
#'        simulations. Supports [`multilayer`] specification.
#' @param set.control.ergm Control arguments passed to `ergm::simulate_formula.network`. In `netsim`,
#'        this is only used when initializing the network with `edapprox = TRUE`. All other
#'        simulations in `netsim` use `tergm`. Supports [`multilayer`] specification.
#' @param set.control.tergm Control arguments passed to `tergm::simulate_formula.network`. See the
#'        help file for [`netdx`] for details and examples on specifying this parameter. Supports
#'        [`multilayer`] specification.
#' @param save.diss.stats If `TRUE`, `netsim` will compute and save duration and dissolution
#'        statistics for plotting and printing, provided `save.network` is `TRUE`, `tergmLite` is
#'        `FALSE`, and the dissolution model is homogeneous.
#' @param dat.updates Either `NULL`, a single function taking arguments `dat`,
#'        `at`, and `network`, or a list of functions of length one greater
#'        than the number of networks being simulated, with each function in
#'        the list taking arguments `dat` and `at`. Here `dat` is the main
#'        `netsim_dat` class object, `at` is the current timestep, and `network`
#'        is an index indicating the current position within the sequence of
#'        network (re)simulations on each time step. If a single function is
#'        passed, it will be called before the first network is simulated and
#'        after each network is simulated, with `network = 0L` before the first
#'        network is simulated and with `network = i` after the `i`th network
#'        is simulated. If a list of functions is passed, the first function
#'        will be called before the first network is simulated, and the
#'        `i + 1`th function will be called after the `i`th network is
#'        simulated. (Note that `at = 0L` is used for initial cross-sectional
#'        simulations in [`sim_nets_t1`].) The function(s) should return the
#'        `netsim_dat` object with any updates needed to correctly represent the
#'        network states for calls to `simulate` and/or `summary`. This can be
#'        useful if nodal attributes appearing in one network model depend on
#'        nodal degrees in a different network.
#' @param ... Additional control settings passed to model.
#'
#' @details
#' `control.net` sets the required control settings for any network model solved with the [`netsim`]
#' function. Controls are required for both base model types and when passing original process
#' modules. For an overview of control settings for base models, consult the
#' [Network Modeling for Epidemics](https://epimodel.github.io/sismid/) course materials For
#' all base models, the `type` argument is a necessary parameter and it has no default.
#'
#' @section The attr.rules Argument:
#' The `attr.rules` parameter is used to specify the rules for how nodal attribute values for
#' incoming nodes should be set. These rules are only necessary for models in which there are
#' incoming nodes (i.e., arrivals). There are three rules available for each attribute value:
#'  * **`current`**: new nodes will be assigned this attribute in proportion to the distribution of
#'    that attribute among existing nodes at that current time step.
#'  * **`t1`**: new nodes will be assigned this attribute in proportion to the distribution of that
#'    attribute among nodes at time 1 (that is, the proportions set in the original network for
#'    [`netest`]).
#'  * **`Value`**: all new nodes will be assigned this specific value, with no variation.
#' For example, the rules list `attr.rules = list(race = "t1", sex = "current", status = "s")`
#' specifies how the race, sex, and status attributes should be set for incoming nodes. By default,
#' the rule is `"current"` for all attributes except status, in which case it is `"s"` (that is, all
#' incoming nodes are susceptible).
#'
#' @section Checkpointing Simulations:
#' `netsim` has a built-in checkpoint system to prevent losing computation work if the function is
#' interrupted (SIGINT, power loss, time limit exceeded on a computation cluster). When enabled,
#' each simulation will be saved every `.checkpoint.steps` time steps. Then, if a checkpoint enabled
#' simulation is launched again with `netsim`, it will restart at the last checkpoint available in
#' the saved data.
#'
#' To enable the checkpoint capabilities of `netsim`, two control arguments have to be set:
#' `.checkpoint.steps`, which is a positive number of time steps to be run between each file save;
#' and `.checkpoint.dir`, which is the path to a directory to save the checkpointed data. If
#' `.checkpoint.dir` directory does not exist, `netsim` will attempt to create it on the first
#' checkpoint save. With these two controls defined, one can simply re-run `netsim` with the same
#' arguments to restart a set of simulations that were interrupted.
#'
#' Simulations are checkpointed individually: for example, if 3 simulations are run on a single core,
#' the first 2 are finished, then the interruption occurs during the third, `netsim` will only
#' restart the third one from the last checkpoint.
#'
#' A `.checkpoint.compress` argument can be set to overwrite the `compress` argument in `saveRDS`
#' used to save the checkpointed data. The current default for `saveRDS` is `gunzip (gz)`, which
#' provides fast compression that usually works well on `netsim` objects.
#'
#' By default, if `netsim` reaches the end of all simulations, the checkpoint data directory and its
#' content are removed before returning the `netsim` object. The `.checkpoint.keep` argument can be
#' set to `TRUE` to prevent this removal to inspect the raw simulation objects.
#'
#' @section New Modules:
#' Base network models use a set of module functions that specify how the individual nodes in the
#' network are subjected to infection, recovery, demographics, and other processes. Core modules are
#' those listed in the `.FUN` arguments. For each module, there is a default function used in
#' the simulation. The default infection module, for example, is contained in the [`infection.net`]
#' function.
#'
#' For original models, one may substitute replacement module functions for any of the default
#' functions. New modules may be added to the workflow at each time step by passing a module function
#' via the `...` argument. Consult the
#' [Extending EpiModel](https://epimodel.github.io/sismid/9_extending/mod9-Intro.html)
#' section of the Network Modeling for Epidemics course materials.
#' One may remove existing modules, such as `arrivals.FUN`, from the workflow by setting
#' the parameter value for that argument to `NULL`.
#'
#' @section End Horizon:
#' `netsim` implements an "End Horizon" mechanism, where a set of modules are
#' removed from the simulation at a specific time step. This is enabled through
#' the `end.horizon` parameter to `control.net`.
#'
#' This parameter must receive a `list` with fields `at`, the time step at which
#' the end horizon occurs, and `modules`, a character vector with the names of
#' the modules to remove. (e.g `list(at = 208, modules = c("arrivals.FUN",
#' "infections.FUN")))
#'
#' @return
#' An EpiModel object of class `control.net`.
#'
#' @seealso
#' Use [`param.net`] to specify model parameters and [`init.net`] to specify the initial conditions.
#' Run the parameterized model with [`netsim`].
#'
#' @keywords parameterization
#'
#' @export
#'
control.net <- function(type,
                        nsteps,
                        start = 1,
                        nsims = 1,
                        ncores = 1,
                        resimulate.network = FALSE,
                        tergmLite = FALSE,
                        cumulative.edgelist = FALSE,
                        truncate.el.cuml = 0,
                        attr.rules,
                        epi.by,
                        initialize.FUN = initialize.net,
                        resim_nets.FUN = resim_nets,
                        summary_nets.FUN = summary_nets,
                        infection.FUN = NULL,
                        recovery.FUN = NULL,
                        departures.FUN = NULL,
                        arrivals.FUN = NULL,
                        nwupdate.FUN = nwupdate.net,
                        prevalence.FUN = prevalence.net,
                        verbose.FUN = verbose.net,
                        module.order = NULL,
                        save.nwstats = TRUE,
                        nwstats.formula = "formation",
                        save.transmat = TRUE,
                        save.network,
                        save.run = FALSE,
                        save.cumulative.edgelist = FALSE,
                        save.other,
                        verbose = TRUE,
                        verbose.int = 1,
                        skip.check = FALSE,
                        raw.output = FALSE,
                        future.use.plan = FALSE,
                        tergmLite.track.duration = FALSE,
                        set.control.ergm = control.simulate.formula(MCMC.burnin = 2e5),
                        set.control.tergm = control.simulate.formula.tergm(MCMC.maxchanges = .Machine$integer.max),
                        save.diss.stats = TRUE,
                        dat.updates = NULL,
                        ...) {

  # Get arguments
  p <- list()
  formal.args <- formals(sys.function())
  formal.args[["..."]] <- NULL
  for (arg in names(formal.args)) {
    if (as.logical(mget(arg) != "")) {
      p[arg] <- list(get(arg))
    }
  }
  dot.args <- list(...)
  names.dot.args <- names(dot.args)
  if (length(dot.args) > 0) {
    for (i in seq_along(dot.args)) {
      p[[names.dot.args[i]]] <- dot.args[[i]]
    }
  }

  if ("births.FUN" %in% names(dot.args)) {
    stop("The births.FUN parameter has been removed. Use arrivals.FUN instead.")
  }
  if ("deaths.FUN" %in% names(dot.args)) {
    stop("The deaths.FUN parameter has been removed. Use departures.FUN instead.")
  }
  if ("depend" %in% names(dot.args)) {
    stop("The depend parameter has been removed. Use resimulate.network instead.")
  }

  ## Module classification
  bi.mods <- grep(".FUN", names(formal.args), value = TRUE)
  p[["bi.mods"]] <- character()
  bi.nms <- bi.mods
  index <- 1
  if (is.null(p[["type"]])) {
    for (i in seq_along(bi.mods)) {
      if (!is.null(p[[bi.mods[i]]])) {
        p[["bi.mods"]][index] <- bi.mods[i]
        index <- index + 1
      }
    }
  } else {
    p[["bi.mods"]] <- bi.mods
  }
  p[["user.mods"]] <- grep(".FUN", names(dot.args), value = TRUE)
  p[["f.names"]] <- c(p[["bi.mods"]], p[["user.mods"]])

  ## Defaults and checks

  #Check whether any base modules have been redefined by user (note: must come
  # after above)
  bi.nms <- bi.nms[-which(bi.nms %in% c("initialize.FUN", "resim_nets.FUN",
                                        "summary_nets.FUN", "verbose.FUN",
                                        "nwupdate.FUN", "prevalence.FUN"))]
  if (length(bi.nms) > 0) {
    flag1 <- logical()
    for (args in seq_along(bi.nms)) {
      if (!(is.null(p[[bi.nms[args]]]))) {
        temp1 <- get(gsub(".FUN", ".net", bi.nms[args]))
        temp2 <- p[[bi.nms[args]]]
        flag1[args] <- identical(temp1, temp2)
      }
    }

    if (!is.null(p[["type"]]) && sum(flag1, na.rm = TRUE) != length(flag1)) {
      stop("Control parameter 'type' must be null if any user defined base
           modules are present")
    }
  }

  if (!is.null(p[["type"]]) && length(p[["user.mods"]]) > 0) {
    stop("Control parameter 'type' must be null if any user specified modules
         are present")
  }

  if (is.null(p[["nsteps"]])) {
    stop("Specify nsteps")
  }

  if (missing(attr.rules)) {
    p[["attr.rules"]] <- list()
  }

  if (!is.null(p[["epi.by"]])) {
    if (length(p[["epi.by"]]) > 1) {
      stop("Length of epi.by currently limited to 1")
    } else {
      p[["epi.by"]] <- epi.by
    }
  }

  if (is.null(p[["save.network"]])) {
    p[["save.network"]] <- TRUE
  }

  if (p[["tergmLite"]] == TRUE && p[["resimulate.network"]] == FALSE) {
    warning("Because tergmLite = TRUE, resetting resimulate.network = TRUE",
            call. = FALSE)
    p[["resimulate.network"]] <- TRUE
  }

  ## Output
  p <- set.control.class("control.net", p)
  return(p)
}


#' @title Cross Checking of Inputs for Stochastic Network Models
#'
#' @description This function checks that the estimation object from
#'              [netest()] and the three parameter lists from
#'              [param.net()], [init.net()], and
#'              [control.net()] are consistent.
#'
#' @param x An `EpiModel` object of class [netest()].
#' @param param An `EpiModel` object of class [param.net()].
#' @param init An `EpiModel` object of class [init.net()].
#' @param control An `EpiModel` object of class [control.net()].
#'
#' @return
#' This function returns no objects.
#'
#' @export
#' @keywords internal
#'
crosscheck.net <- function(x, param, init, control) {
  check.control.class("net", "EpiModel crosscheck.net")

  if (!is.null(control[["type"]]) && length(control[["user.mods"]]) == 0) {

    if (control[["start"]] == 1 && control[["skip.check"]] == FALSE) {

      # Main class check ----------------------------------------------------
      if (inherits(x, "netest")) {
        x <- list(x)
      }
      if (!inherits(x, "list") || length(x) == 0 ||
            !all(vapply(x, inherits, logical(1), "netest"))) {
        stop("x must be either an object of class netest or a list of objects",
             " of class netest when start == 1")
      }
      if (!inherits(param, "param.net")) {
        stop("param must be an object of class param.net")
      }
      if (!inherits(init, "init.net")) {
        stop("init must be an object of class init.net")
      }
      if (!inherits(control, "control.net")) {
        stop("control must be an object of class control.net")
      }

      # Pull network object from netest object
      nw <- x[[1]]$newnetwork

      # Defaults ------------------------------------------------------------

      # Is status in network formation formula?
      statOnNw <- get_vertex_attribute(nw, "status") %in% c("s", "i", "r")
      statOnNw <- ifelse(sum(statOnNw) > 0, TRUE, FALSE)

      nGroups <- length(unique(get_vertex_attribute(nw, "group")))
      nGroups <- ifelse(nGroups == 2, 2, 1)

      if (nGroups == 2 && is.null(control[["pid.prefix"]])) {
        control[["pid.prefix"]] <- c("g1.", "g2.")
      }

      if (statOnNw == TRUE && is.null(control[["attr.rules"]][["status"]])) {
        control[["attr.rules"]][["status"]] <- "s"
      }


      # Checks -------------------------------------------------------------

      # Check that prevalence in NW attr status and initial conditions match
      if (statOnNw == TRUE) {
        nw1 <- sum(get_vertex_attribute(nw, "status") == 1)
        init1 <- sum(unlist(init[grep("i.num", names(init), value = TRUE)]))
        if ("i.num" %in% names(init) && nw1 != init1) {
          warning("Overriding init infected settings with network
                  status attribute", immediate. = TRUE)
        }
      }

      # Check consistency of status vector to network structure
      if (!is.null(init[["status.vector"]])) {
        if (length(init[["status.vector"]]) != network.size(nw)) {
          stop("Length of status.vector is unequal to size of initial network")
        }
        svals <- sort(unique(init[["status.vector"]]))
        if (control[["type"]] == "SIR") {
          if (any(svals %in% c("s", "i", "r") == FALSE)) {
            stop("status.vector contains values other than \"s\", \"i\",
                 and \"r\" ")
          }
        } else {
          if (any(svals %in% c("s", "i") == FALSE)) {
            stop("status.vector contains values other than \"s\" and \"i\" ")
          }
        }
      }

      # Two-group model checks for inital conditions
      if (nGroups == 2 && is.null(init[["i.num.g2"]]) &&
            is.null(init[["status.vector"]]) && statOnNw == FALSE) {
        stop("Specify i.num.g2 for two-group model simulations")
      }

      # Recovery rate and initial recovered checks
      if (control[["type"]] %in% c("SIR", "SIS")) {
        if (is.null(param[["rec.rate"]])) {
          stop("Specify rec.rate in param.net")
        }
        if (nGroups == 2 && is.null(param[["rec.rate.g2"]])) {
          stop("Specify rec.rate.g2 in param.net")
        }
      }
      if (control[["type"]] == "SIR") {
        if (is.null(init[["r.num"]]) && is.null(init[["status.vector"]]) && statOnNw == FALSE) {
          stop("Specify r.num in init.net")
        }
        if (nGroups == 2 && is.null(init[["r.num.g2"]]) &&
              is.null(init[["status.vector"]]) && statOnNw == FALSE) {
          stop("Specify r.num.g2 in init.net")
        }
      }

      # Check demographic parameters for two-group models
      if (nGroups == 2 && param[["vital"]] == TRUE) {
        if (is.null(param[["a.rate.g2"]])) {
          stop("Specify a.rate.g2 in param.net")
        }
        if (is.null(param[["ds.rate.g2"]])) {
          stop("Specify ds.rate.g2 in param.net")
        }
        if (is.null(param[["di.rate.g2"]])) {
          stop("Specify di.rate.g2 in param.net")
        }
        if (control[["type"]] == "SIR") {
          if (is.null(param[["dr.rate.g2"]])) {
            stop("Specify dr.rate.g2 in param.net")
          }
        }
      }
    }

    if (control[["start"]] > 1) {

      control[["resimulate.network"]] <- TRUE

      if (control[["skip.check"]] == FALSE) {
        if (!inherits(x, "netsim")) {
          stop("x must be a netsim object if control setting start > 1")
        }
        if (is.null(x[["run"]])) {
          stop("x must contain `run` to restart simulation, see `save.run` ",
               "control setting")
        }
        if (control[["nsteps"]] < control[["start"]]) {
          stop("control setting nsteps must be > control setting start in ",
               "restarted simulations")
        }
        if (control[["start"]] > x[["control"]][["nsteps"]] + 1) {
          stop("control setting start must be 1 greater than the nsteps in
               the ", "prior simulation")
        }
      }
    }


    ## Assign modules based on group parameter
    if (!is.null(control[["type"]])) {
      def <- grep(".FUN", names(control))
      args <- names(control)[def]
      flag <- length(grep(".g2", names(param)))

      if (flag == 0) {
        for (i in seq_along(args)) {
          if (is.null(control[[args[i]]])) {
            temp <- get(gsub(".FUN", ".net", args[i]))
            control[[args[i]]] <- temp
            control[["f.args"]][i] <- gsub(".FUN", ".net", args[i])
          }
        }
      } else {
        for (i in seq_along(args)) {
          if (is.null(control[[args[i]]])) {
            temp <- get(gsub(".FUN", ".2g.net", args[i]))
            control[[args[i]]] <- temp
            control[["f.args"]][i] <- gsub(".FUN", ".2g.net", args[i])
          }
        }
      }
    }
  }

  if (!is.null(control[["type"]]) && length(control[["user.mods"]]) > 0) {
    stop("Control setting 'type' must be NULL if any user-specified modules
         specified.")
  }

  if (inherits(x, "netest")) {
    nwparam <- list(x)
  } else if (inherits(x, "netsim")) {
    nwparam <- x$nwparam
  } else if (inherits(x, "networkDynamic")) {
    nwparam <- list(x) # relevant to EpiModel gallery example
  } else { # must be list of netest
    nwparam <- x
  }

  num.nw <- length(nwparam)

  # convert relevant control arguments to multilayer if they are not already, and check length
  for (control_arg_name in c("nwstats.formula", "set.control.ergm",
                             "set.control.tergm", "tergmLite.track.duration")) {
    control_arg_value <- control[[control_arg_name]]
    if (!is(control_arg_value, "multilayer")) {
      control[[control_arg_name]] <- do.call(multilayer, rep(list(control_arg_value), length.out = num.nw))
    } else if (length(control_arg_value) != num.nw) {
      stop("multilayer control argument `", control_arg_name, "` has length ",
           length(control_arg_value), " but should have length ", num.nw, ".")
    }
  }

  # convert nwstats.formula = "formation" to actual formation formula
  for (network in seq_len(num.nw)) {
    if (!is.null(control[["nwstats.formula"]][[network]]) &&
          control[["nwstats.formula"]][[network]] == "formation") {
      control[["nwstats.formula"]][[network]] <- nwparam[[network]]$formation
    }
  }

  # convert dat.updates to a function of dat, at, network if it is not already
  dat.updates <- control[["dat.updates"]]
  if (is.list(dat.updates)) {
    if (length(dat.updates) != num.nw + 1L) {
      stop("control argument `dat.updates` is of length ", length(dat.updates),
           " but should be of length ", num.nw + 1L, ".")
    }
    control[["dat.updates"]] <- trim_env(function(dat, at, network) {
      dat.updates[[network + 1L]](dat = dat, at = at)
    }, keep = c("dat.updates"))
  }

  ## In-place assignment to update param and control
  assign("param", param, pos = parent.frame())
  assign("control", control, pos = parent.frame())
}

#' @title Specify Controls by Network
#'
#' @description This utility function allows specification of certain
#'              [netsim()] controls to vary by network.  The
#'              [netsim()] control arguments currently supporting
#'              `multilayer` specifications are `nwstats.formula`,
#'              `set.control.ergm`, `set.control.tergm`, and
#'              `tergmLite.track.duration`.
#'
#' @param ... control arguments to apply to each network, with the index of the
#'        network corresponding to the index of the control argument
#'
#' @return an object of class `multilayer` containing the specified
#'         control arguments
#'
#' @export
#'
multilayer <- function(...) {
  out <- list(...)
  class(out) <- c("multilayer", class(out))
  return(out)
}
