R/add_constraint_targets.R

Defines functions add_constraint_targets_relative add_constraint_targets_absolute

Documented in add_constraint_targets_absolute add_constraint_targets_relative

#' @title Add absolute targets
#'
#' @description
#' Add feature-level absolute targets to a planning problem.
#'
#' These targets are stored in the problem object and later translated into
#' linear constraints when the optimization model is built. Absolute targets
#' are interpreted directly in the same units as the feature
#' contributions used by the model. Each call appends one or more target definitions to the problem. This makes
#' it possible to combine multiple target rules, including targets associated
#' with different action subsets.
#'
#' @details
#' Calls accumulate targets for distinct feature/action scopes. A second target
#' for the same feature and action scope raises an error immediately, even if
#' it uses relative units or a different label. To change a target, rebuild
#' from the problem before it was added.
#'
#' Use this function when target requirements are naturally expressed in the
#' original units of the modelled feature contributions, rather than as
#' proportions of reference-scenario totals.
#'
#' Let \eqn{\mathcal{F}} denote the set of features. For each targeted feature
#' \eqn{f \in \mathcal{F}}, this function stores an absolute target threshold
#' \eqn{T_f \ge 0}.
#'
#' With at most one action per unit, each target is interpreted as a
#' lower-bound constraint on post-action amounts of the form:
#' \deqn{
#' \sum_{(i,a) \in \mathcal{D}_f^{\star}} c_{iaf} x_{ia} \ge T_f,
#' }
#' where:
#' \itemize{
#'   \item \eqn{i \in \mathcal{I}} indexes planning units,
#'   \item \eqn{a \in \mathcal{A}} indexes actions,
#'   \item \eqn{x_{ia}} indicates whether action \eqn{a} is selected in planning
#'   unit \eqn{i},
#'   \item \eqn{c_{iaf}} is the contribution of that action to feature
#'   \eqn{f},
#'   \item \eqn{\mathcal{D}_f^{\star}} is the subset of planning unit--action
#'   pairs allowed to count toward the target for feature \eqn{f}.
#' }
#'
#' In the absolute case, the stored target threshold is simply:
#' \deqn{
#' T_f = t_f,
#' }
#' where \eqn{t_f} is the user-supplied target value for feature \eqn{f}.
#'
#' With concurrent actions, the model instead combines signed changes within
#' each unit and feature and counts the reference once:
#' \deqn{\sum_i \left(r_{if} s_i^{\star} + \Delta_{if}^{\star}(x)\right) \ge T_f.}
#' Here \eqn{s_i^{\star}} indicates that at least one action in the target scope
#' is selected. Joint corrections contribute only when every member belongs
#' to that scope. Individual effects are additive when no interaction is supplied.
#' The same aggregation is used to report target achievement.
#'
#' The \code{actions} argument restricts which actions may contribute toward
#' achievement of the target, but it does not modify the value of \eqn{T_f}
#' itself.
#'
#' The \code{targets} argument is parsed by \code{.pa_parse_targets()} and may be
#' supplied in several equivalent forms, including:
#' \itemize{
#'   \item a single numeric value recycled to all selected features,
#'   \item a numeric vector aligned to \code{features},
#'   \item a named numeric vector where names identify features,
#'   \item a \code{data.frame} with \code{feature} and \code{target} columns.
#' }
#'
#' If \code{targets} does not explicitly identify features:
#' \itemize{
#'   \item if \code{features = NULL}, the target is applied to all features,
#'   \item if \code{features} is supplied, the target values are interpreted with
#'   respect to that feature set.
#' }
#'
#' Repeated calls append new target rules rather than replacing previous ones.
#' This allows cumulative target modelling, including multiple rules on the same
#' feature with different contributing action subsets.
#'
#' @param x A \code{Problem} object.
#' @param targets Target specification. This is interpreted as an absolute target
#'   value in the same units as the modelled feature contributions. It may be a
#'   scalar, vector, named vector, or \code{data.frame}. See Details.
#' @param features Optional feature specification indicating which features the
#'   supplied target values refer to when \code{targets} does not identify
#'   features explicitly. If \code{NULL}, all features are targeted.
#' @param actions Optional character vector indicating which actions count toward
#'   target achievement. Entries may match action ids, \code{action_set} labels,
#'   or both. If \code{NULL}, all actions count.
#' @param label Optional character string stored with the targets for reporting
#'   and bookkeeping.
#'
#' @return An updated \code{Problem} object with absolute targets appended to
#'   the stored target table.
#'
#' @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
#'   )
#'
#' # Same absolute target for all features
#' p1 <- add_constraint_targets_absolute(p, 3)
#' p1$data$targets
#'
#' # Different targets by feature
#' p2 <- add_constraint_targets_absolute(
#'   p,
#'   c("1" = 4, "2" = 2)
#' )
#' p2$data$targets
#'
#' # Restrict which actions count toward target achievement
#' p3 <- add_constraint_targets_absolute(
#'   p,
#'   2,
#'   actions = "protect"
#' )
#' p3$data$targets
#'
#' @seealso
#' \code{\link{add_constraint_targets_relative}}
#'
#' @export
add_constraint_targets_absolute <- function(x, targets,
                                 features = NULL,
                                 actions = NULL,
                                 label = NULL) {
  stopifnot(inherits(x, "Problem"))

  x <- .pa_clone_data(x)
  dt <- .pa_parse_targets(x, targets, features = features)

  actions_txt <- .pa_target_actions(x, actions)

  out <- data.frame(
    feature      = as.numeric(dt$feature),
    type         = "actions",
    sense        = "ge",
    target_unit  = "absolute",
    target_raw   = as.numeric(dt$target_raw),
    basis_total  = NA_real_,
    target_value = as.numeric(dt$target_raw),
    actions      = actions_txt,
    label        = if (is.null(label)) NA_character_ else as.character(label),
    created_at   = as.character(Sys.time()),
    stringsAsFactors = FALSE
  )

  out <- .pa_add_feature_labels(
    df = out,
    features_df = x$data$features,
    feature_col = "feature",
    internal_feature_col = "internal_feature",
    out_col = "feature_name"
  )

  x$data$meta$model_dirty <- TRUE
  .pa_store_targets(x, out)
}

#' @title Add relative targets
#'
#' @description
#' Add feature-level relative targets to a planning problem.
#'
#' These targets are stored in the problem object and later translated into
#' linear constraints when the optimization model is built. Relative targets are
#' supplied as proportions in \eqn{[0,1]} and are converted internally into
#' absolute thresholds using the current total amount of each
#' feature in the landscape.
#'
#' Each call appends one or more target definitions to the problem. This makes
#' it possible to combine multiple target rules, including targets associated
#' with different action subsets.
#'
#' @details
#' Use this function when target requirements are naturally expressed as
#' proportions of reference-scenario feature totals rather than in original
#' feature units.
#'
#' Let \eqn{\mathcal{F}} denote the set of features. For each targeted feature
#' \eqn{f \in \mathcal{F}}, let \eqn{B_f} denote the reference-scenario total
#' amount of that feature in the landscape, as computed by
#' \code{.pa_feature_totals()}.
#'
#' If the user supplies a relative target \eqn{r_f \in [0,1]}, then this
#' function converts it to an absolute threshold:
#' \deqn{
#' T_f = r_f \times B_f.
#' }
#'
#' The absolute threshold \eqn{T_f} is stored in \code{target_value}, while:
#' \itemize{
#'   \item the original user-supplied proportion \eqn{r_f} is stored in
#'   \code{target_raw},
#'   \item the baseline total \eqn{B_f} is stored in \code{basis_total}.
#' }
#'
#' When the optimization model is built, the resulting target is interpreted as:
#' \deqn{
#' \sum_{(i,a) \in \mathcal{D}_f^{\star}} c_{iaf} x_{ia} \ge T_f,
#' }
#' where:
#' \itemize{
#'   \item \eqn{i \in \mathcal{I}} indexes planning units,
#'   \item \eqn{a \in \mathcal{A}} indexes actions,
#'   \item \eqn{x_{ia}} indicates whether action \eqn{a} is selected in planning
#'   unit \eqn{i},
#'   \item \eqn{c_{iaf}} is the contribution of that action to feature
#'   \eqn{f},
#'   \item \eqn{\mathcal{D}_f^{\star}} is the subset of planning unit--action
#'   pairs allowed to count toward the target for feature \eqn{f}.
#' }
#'
#' With concurrent actions, the model instead combines signed changes within
#' each unit and feature and counts the reference once:
#' \deqn{\sum_i \left(r_{if} s_i^{\star} + \Delta_{if}^{\star}(x)\right) \ge T_f.}
#' Here \eqn{s_i^{\star}} indicates that at least one action in the target scope
#' is selected. Joint corrections contribute only when every member belongs
#' to that scope. Individual effects are additive when no interaction is supplied.
#' The same aggregation is used to report target achievement.
#'
#' The \code{actions} argument restricts which actions may contribute toward
#' target achievement, but it does not affect the baseline amount \eqn{B_f} used
#' to compute the threshold. In other words, relative targets are always scaled
#' against the full landscape reference scenario.
#'
#' Therefore, \code{actions} changes who may satisfy the target, but not how the
#' threshold itself is scaled.
#'
#' The \code{targets} argument is parsed by \code{.pa_parse_targets()} and may be
#' supplied in several equivalent forms, including:
#' \itemize{
#'   \item a single numeric value recycled to all selected features,
#'   \item a numeric vector aligned to \code{features},
#'   \item a named numeric vector where names identify features,
#'   \item a \code{data.frame} with \code{feature} and \code{target} columns.
#' }
#'
#' If \code{targets} does not explicitly identify features:
#' \itemize{
#'   \item if \code{features = NULL}, the target is applied to all features,
#'   \item if \code{features} is supplied, the target values are interpreted with
#'   respect to that feature set.
#' }
#'
#' Relative targets must lie in \eqn{[0,1]}.
#' A second target for the same feature and action scope raises an error,
#' including when an absolute target already exists. Labels and threshold
#' values do not change target identity.
#'
#' Repeated calls append new target rules rather than replacing previous ones.
#' This allows cumulative target modelling, including multiple rules on the same
#' feature with different contributing action subsets.
#'
#' @param x A \code{Problem} object.
#' @param targets Target specification as proportions in \eqn{[0,1]}. It may be
#'   a scalar, vector, named vector, or \code{data.frame}. See Details.
#' @param features Optional feature specification indicating which features the
#'   supplied target values refer to when \code{targets} does not identify
#'   features explicitly. If \code{NULL}, all features are targeted.
#' @param actions Optional character vector indicating which actions count toward
#'   target achievement. Entries may match action ids, \code{action_set} labels,
#'   or both. If \code{NULL}, all actions count.
#' @param label Optional character string stored with the targets for reporting
#'   and bookkeeping.
#'
#' @return An updated \code{Problem} object with relative targets appended to
#'   the stored target table.
#'
#' @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
#'   )
#'
#' # Require 30% of the baseline total for all features
#' p1 <- add_constraint_targets_relative(p, 0.3)
#' p1$data$targets
#'
#' # Require 20% for one selected feature
#' p2 <- add_constraint_targets_relative(
#'   p,
#'   0.2,
#'   features = 1
#' )
#' p2$data$targets
#'
#' # Restrict which actions count toward target achievement
#' p3 <- add_constraint_targets_relative(
#'   p,
#'   0.2,
#'   actions = "protect"
#' )
#' p3$data$targets
#'
#' @seealso
#' \code{\link{add_constraint_targets_absolute}}
#'
#' @export
add_constraint_targets_relative <- function(x, targets,
                                 features = NULL,
                                 actions = NULL,
                                 label = NULL) {
  stopifnot(inherits(x, "Problem"))

  x <- .pa_clone_data(x)
  dt <- .pa_parse_targets(x, targets, features = features)

  rel <- as.numeric(dt$target_raw)
  if (any(rel < 0 | rel > 1, na.rm = TRUE)) {
    stop("Relative targets must be between 0 and 1.", call. = FALSE)
  }

  basis <- .pa_feature_totals(x)
  basis_v <- basis[as.character(dt$feature)]
  basis_v[is.na(basis_v)] <- 0
  abs_target <- rel * as.numeric(basis_v)

  actions_txt <- .pa_target_actions(x, actions)

  out <- data.frame(
    feature      = as.numeric(dt$feature),
    type         = "actions",
    sense        = "ge",
    target_unit  = "relative_baseline",
    target_raw   = rel,
    basis_total  = as.numeric(basis_v),
    target_value = as.numeric(abs_target),
    actions      = actions_txt,
    label        = if (is.null(label)) NA_character_ else as.character(label),
    created_at   = as.character(Sys.time()),
    stringsAsFactors = FALSE
  )

  out <- .pa_add_feature_labels(
    df = out,
    features_df = x$data$features,
    feature_col = "feature",
    internal_feature_col = "internal_feature",
    out_col = "feature_name"
  )

  x$data$meta$model_dirty <- TRUE
  .pa_store_targets(x, out)
}

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.