R/add_constraint_area.R

Defines functions add_constraint_area

Documented in add_constraint_area

#' @include internal.R
#'
#' @title Add area constraint
#'
#' @description
#' Add an area constraint to a planning problem.
#'
#' This function stores one area-constraint specification in the
#' \code{Problem} object so that it can later be incorporated when the
#' optimization model is assembled. Area constraints can be imposed either on
#' the total selected planning-unit area or on the effective area of selected
#' actions.
#'
#' Multiple area constraints can be added by calling this function repeatedly,
#' provided that no duplicated combination of action subset and constraint sense
#' is introduced.
#'
#' @details
#' Calls accumulate constraints for distinct action scopes, area measures
#' (\code{area_col}), or senses. Repeating the same combination raises an error,
#' even with a different threshold, display unit, or name. To change it, rebuild
#' from the problem before the constraint was added.
#' Explicit names must be unique within the area-constraint family; omitted
#' names are generated automatically.
#'
#' Use this function when area requirements must be imposed either on the total
#' selected landscape or on the effective area allocated to specific actions.
#'
#' \strong{Total selected area.}
#'
#' Let \eqn{\mathcal{I}} denote the set of planning units and let
#' \eqn{a_i \ge 0} be the area associated with planning unit
#' \eqn{i \in \mathcal{I}}.
#'
#' When \code{actions = NULL}, the constraint refers to the total selected area
#' in the problem. In that case, let \eqn{w_i \in \{0,1\}} denote the binary
#' variable indicating whether planning unit \eqn{i} is selected by at least one
#' decision in the model.
#'
#' Depending on \code{sense}, this function stores one of the following
#' constraints.
#'
#' If \code{sense = "min"}:
#' \deqn{
#' \sum_{i \in \mathcal{I}} a_i w_i \ge A
#' }
#'
#' If \code{sense = "max"}:
#' \deqn{
#' \sum_{i \in \mathcal{I}} a_i w_i \le A
#' }
#'
#' If \code{sense = "equal"} and \code{tolerance = 0}:
#' \deqn{
#' \sum_{i \in \mathcal{I}} a_i w_i = A
#' }
#'
#' If \code{sense = "equal"} and \code{tolerance > 0}, the equality is stored as
#' a two-sided band:
#' \deqn{
#' A - \tau \le \sum_{i \in \mathcal{I}} a_i w_i \le A + \tau
#' }
#' where \eqn{\tau} is the value supplied through \code{tolerance}.
#'
#' \strong{Action-specific effective area.}
#'
#' When \code{actions} is not \code{NULL}, the constraint refers to the
#' effective area of the selected decisions associated with the specified subset
#' of actions.
#'
#' Let \eqn{\mathcal{A}^\star \subseteq \mathcal{A}} denote the specified
#' action subset and let \eqn{x_{ia} \in \{0,1\}} denote the binary variable
#' indicating whether action \eqn{a \in \mathcal{A}^\star} is selected in
#' planning unit \eqn{i \in \mathcal{I}}. Let \eqn{b_{ia} \ge 0} denote the
#' effective action area associated with feasible pair \eqn{(i,a)}, as stored in
#' \code{x$data$dist_actions$action_area}. In that case, the constrained
#' quantity is:
#' \deqn{
#' \sum_{i \in \mathcal{I}} \sum_{a \in \mathcal{A}^\star} b_{ia} x_{ia}.
#' }
#'
#' Effective action areas are defined in \code{\link{add_actions}}. They can be
#' supplied manually through the \code{action_area} argument, derived from
#' spatial \code{include_pairs}, or default to full planning-unit areas when
#' these can be derived from the problem.
#'
#' Consequently, if no partial action areas are supplied and each feasible action
#' is assumed to apply to the full planning unit, then an action-specific area
#' constraint is numerically equivalent to using planning-unit areas for those
#' action decisions. If partial action areas are supplied, the constraint uses
#' those effective areas instead.
#'
#' \strong{Area units and area sources.}
#'
#' The value of \code{area_unit} indicates the unit in which \code{area} and
#' \code{tolerance} are expressed.
#'
#' When \code{actions = NULL}, areas are obtained from the planning-unit table or
#' planning-unit geometry. If \code{area_col} is provided, that column is used.
#' Otherwise, the model builder determines the default area source according to
#' the internal rules of the package.
#'
#' When \code{actions} is not \code{NULL}, the model builder uses
#' \code{x$data$dist_actions$action_area}. These values are stored internally in
#' square metres and are converted to \code{area_unit} when the model is built.
#' If action areas are missing for relevant feasible \code{(pu, action)} pairs,
#' the model builder may use \code{area_col} as a fallback to derive full
#' planning-unit areas. If valid action areas still cannot be obtained, model
#' construction fails with an error.
#'
#' This function only stores the constraint specification; it does not validate
#' the feasibility of the threshold against the available planning units or
#' actions at this stage.
#'
#' Multiple area constraints can be stored in a \code{Problem} object. However,
#' at most one can be stored for the same combination of action subset and
#' constraint sense. Attempting to add a duplicated
#' \code{actions}--\code{sense} combination results in an error.
#'
#' @param x A \code{Problem} object.
#'
#' @param area Numeric scalar greater than or equal to zero. Target value for
#'   the constrained area.
#'
#' @param sense Character string indicating the type of area constraint. Must be
#'   one of \code{"min"}, \code{"max"}, or \code{"equal"}.
#'
#' @param tolerance Numeric scalar greater than or equal to zero. Only used when
#'   \code{sense = "equal"}. In that case, equality is interpreted as a band
#'   around \code{area} with half-width \code{tolerance}. Ignored otherwise.
#'
#' @param area_col Optional character string giving the name of the planning-unit
#'   area column. When \code{actions = NULL}, this column is used as the source
#'   of planning-unit areas. When \code{actions} is not \code{NULL}, the
#'   constraint uses \code{x$data$dist_actions$action_area}; \code{area_col} can
#'   still be used by the model builder as a fallback if action areas are missing
#'   and full planning-unit areas can be derived.
#'
#' @param area_unit Character string indicating the unit of \code{area} and
#'   \code{tolerance}. Must be one of \code{"m2"}, \code{"ha"}, or
#'   \code{"km2"}.
#'
#' @param actions Optional subset of actions to which the constraint applies.
#'   If \code{NULL}, the constraint applies to the total selected planning-unit
#'   area through the planning-unit selection variables. Otherwise, it applies to
#'   the effective area of the selected decision variables associated with the
#'   specified subset of actions, using
#'   \code{x$data$dist_actions$action_area}. This argument is resolved using the
#'   package's standard action subset parser.
#'
#' @param name Optional character string used as the label of the stored linear
#'   constraint when it is later added to the optimization model. If
#'   \code{NULL}, a default name is generated.
#'
#' @return An updated \code{Problem} object with the new area-constraint
#'   specification appended to \code{x$data$constraints$area}.
#'
#' @seealso
#' \code{\link{create_problem}},
#' \code{\link{add_actions}}
#'
#' @examples
#' # Load a complete simulated planning problem.
#' example_data <- load_sim_multiaction()
#'
#' p <- create_problem(
#'   pu = example_data$planning_units,
#'   features = example_data$features,
#'   dist_features = example_data$dist_features,
#'   cost = "cost"
#' ) |>
#'   add_actions(
#'     example_data$actions,
#'     cost = example_data$action_costs
#'   )
#'
#' # Constrain the total selected planning-unit area.
#' p <- add_constraint_area(
#'   x = p,
#'   area = 25,
#'   sense = "min",
#'   area_unit = "ha"
#' )
#'
#' # Constrain the effective area allocated to the restore action.
#' # Action areas are derived from the planning-unit geometries.
#'
#' p <- add_constraint_area(
#'   x = p,
#'   area = 15,
#'   sense = "max",
#'   area_unit = "ha",
#'   actions = "restore"
#' )
#'
#' p$data$constraints$area
#'
#' @export
add_constraint_area <- function(x,
                                area,
                                sense,
                                tolerance = 0,
                                area_col = NULL,
                                area_unit = c("m2", "ha", "km2"),
                                actions = NULL,
                                name = NULL) {

  stopifnot(inherits(x, "Problem"))

  if (missing(sense) || is.null(sense)) {
    stop(
      "`sense` must be explicitly provided. Use one of: 'min', 'max', or 'equal'.",
      call. = FALSE
    )
  }

  sense <- match.arg(sense, c("min", "max", "equal"))
  area_unit <- match.arg(area_unit)

  assertthat::assert_that(
    assertthat::is.number(area),
    is.finite(area),
    area >= 0
  )

  assertthat::assert_that(
    assertthat::is.number(tolerance),
    is.finite(tolerance),
    tolerance >= 0
  )

  if (!is.null(area_col)) {
    assertthat::assert_that(
      is.character(area_col),
      length(area_col) == 1,
      !is.na(area_col),
      nzchar(area_col)
    )
  }

  if (!is.null(name)) {
    if (!is.character(name) || length(name) != 1 || is.na(name) || !nzchar(name)) {
      stop("`name` must be NULL or a non-empty character string.", call. = FALSE)
    }
  }

  if (sense != "equal" && tolerance > 0) {
    warning(
      "`tolerance` is only used when sense = 'equal'. It will be ignored.",
      call. = FALSE,
      immediate. = TRUE
    )
  }

  x <- .pa_clone_data(x)

  actions_txt <- NA_character_

  if (!is.null(actions)) {
    act_subset <- .pa_resolve_action_subset(x, subset = actions)

    if (!is.data.frame(act_subset) || nrow(act_subset) == 0) {
      stop("`actions` did not resolve to any valid actions.", call. = FALSE)
    }

    actions_txt <- .pa_subset_to_string(act_subset$id)
  }

  area_name <- if (is.null(name)) {
    if (is.na(actions_txt)) {
      paste0("area_", sense)
    } else {
      paste0("area_", sense, "_", gsub("\\|", "_", actions_txt))
    }
  } else {
    as.character(name)[1]
  }

  area_name <- .pa_constraint_name(x, "area", area_name, explicit = !is.null(name))

  area_df <- data.frame(
    type = "area",
    sense = sense,
    value = as.numeric(area),
    tolerance = if (sense == "equal") as.numeric(tolerance) else 0,
    unit = area_unit,
    area_col = if (is.null(area_col)) NA_character_ else as.character(area_col)[1],
    actions = actions_txt,
    name = area_name,
    stringsAsFactors = FALSE
  )

  x <- .pa_store_area_constraints(x, area_df)

  if (!is.null(x$data$model_ptr)) {
    x$data$meta <- x$data$meta %||% list()
    x$data$meta$model_dirty <- TRUE
  }

  x
}

Try the multiscape package in your browser

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

multiscape documentation built on Oct. 12, 2026, 5:08 p.m.