Define the action catalogue, the set of feasible planning unit–action pairs, and their implementation costs.
This function adds two core components to a Problem object. First, it
stores the action catalogue. Second, it creates the feasible planning
unit–action table, including implementation costs, status codes, and
internal indices used by the optimization backend.
Conceptually, if \(\mathcal{I}\) is the set of planning units and \(\mathcal{A}\) is the set of actions, this function determines which pairs \((i,a) \in \mathcal{I} \times \mathcal{A}\) are feasible decisions and assigns a non-negative implementation cost to each feasible pair.
Arguments
- x
A
Problemobject created withcreate_problem.- actions
A
data.framedefining the action catalogue. It must contain a uniqueidcolumn. A column namedactionis also accepted and automatically renamed toid.- include_pairs
Optional specification of feasible
(pu, action)pairs. It can beNULL, adata.framewith columnspuandaction(optionally alsofeasible), or a named list whose names are action ids and whose elements are vectors of planning unit ids orsfobjects.- exclude_pairs
Optional specification of infeasible
(pu, action)pairs. It uses the same formats asinclude_pairsand removes matching pairs from the feasible set.- cost
Optional cost specification for feasible pairs. It may be
NULL, a scalar numeric value, a named numeric vector indexed by action id, or adata.framewith columnsaction, costorpu, action, cost.
Value
An updated Problem object with:
actionsThe action catalogue, including a unique integer
internal_idfor each action.dist_actionsThe feasible planning unit–action table with columns
pu,action,cost,status,internal_pu, andinternal_action.pu indexA mapping from user-supplied planning-unit ids to internal integer ids.
action indexA mapping from action ids to internal integer ids.
Details
When to use add_actions().
Use this function when you want to move from a planning problem defined only by planning units and features to a problem in which decisions are explicitly represented as actions applied in planning units.
Action catalogue.
The actions argument must be a data.frame with a unique
id column identifying each action. If a column named action is
supplied instead, it is renamed internally to id. Additional columns
are preserved. If no name column is provided, action labels are taken
from id. If an action_set column is present, it is also
preserved and can later be used to refer to groups of actions.
Actions are stored sorted by id to ensure reproducible internal
indexing.
Feasible planning unit–action pairs.
Feasibility is controlled through include_pairs and
exclude_pairs.
If include_pairs = NULL, all possible (pu, action) pairs are
initially considered feasible, that is, all pairs
\((i,a) \in \mathcal{I} \times \mathcal{A}\).
If include_pairs is supplied, only those pairs are retained. If
exclude_pairs is also supplied, matching pairs are removed
afterwards.
More precisely, let \(\mathcal{D}^{\mathrm{inc}}\) denote the set of included planning unit–action pairs and let \(\mathcal{D}^{\mathrm{exc}}\) denote the set of excluded pairs.
If include_pairs = NULL, the feasible decision set is:
$$
\{(i,a) : i \in \mathcal{I},\ a \in \mathcal{A}\} \setminus \mathcal{D}^{\mathrm{exc}}.
$$
If include_pairs is supplied, the feasible decision set is:
$$
\mathcal{D}^{\mathrm{inc}} \setminus \mathcal{D}^{\mathrm{exc}}.
$$
Both include_pairs and exclude_pairs can be specified as:
NULL,a
data.framewith columnspuandaction,or a named list whose names are action ids.
When supplied as a data.frame, the object must contain columns
pu and action. An optional logical-like column
feasible may also be provided; only rows with feasible = TRUE
are retained. Missing values in feasible are treated as
FALSE.
When supplied as a named list, names must match action ids. Each element may contain either:
a vector of planning-unit ids, or
an
sfobject defining the spatial zone where the action is feasible.
In the spatial case, feasible planning units are identified using
sf::st_intersects() against the stored planning-unit geometry.
Feasibility versus decision fixing.
This function only determines whether a pair \((i,a)\) exists in the model.
It does not force a feasible action to be selected or forbidden beyond
structural infeasibility. Fixed decisions should instead be imposed later
with add_constraint_locked_actions.
Costs.
Costs can be supplied in several ways:
If
cost = NULL, all feasible pairs receive a default cost of1.If
costis a scalar, that value is assigned to all feasible pairs.If
costis a named numeric vector, names must match action ids and costs are assigned by action.If
costis adata.frame, it must define either:action-level costs through columns
actionandcost, orpair-specific costs through columns
pu,action, andcost.
In all cases, costs must be finite and non-negative.
In practice, a scalar cost is useful when all actions cost the same
everywhere, a named vector is useful when cost depends only on action type,
and a (pu, action, cost) table is useful when cost varies by both
planning unit and action.
Status values.
Internally, all feasible pairs are initialized with status = 0,
meaning that the decision is free. If planning units have already been marked
as locked out, then all feasible actions in those planning units are assigned
status = 3. This preserves consistency with planning-unit exclusions
already stored in the problem.
Replacement behaviour.
Calling add_actions() replaces any previous action catalogue and
feasible action table stored in the problem object.
After defining actions, typical next steps include adding effects, optional
decision-fixing constraints, objectives, and solver settings before calling
solve().
Examples
# ------------------------------------------------------
# Minimal planning problem
# ------------------------------------------------------
pu <- data.frame(
id = 1:4,
cost = c(2, 3, 1, 4)
)
features <- data.frame(
id = 1:2,
name = c("sp1", "sp2")
)
dist_features <- data.frame(
pu = c(1, 1, 2, 3, 4, 4),
feature = c(1, 2, 1, 2, 1, 2),
amount = c(1, 2, 1, 3, 2, 1)
)
p <- create_problem(
pu = pu,
features = features,
dist_features = dist_features
)
actions <- data.frame(
id = c("conservation", "restoration"),
name = c("Conservation", "Restoration")
)
# Example 1: all actions feasible in all planning units
p1 <- add_actions(
x = p,
actions = actions,
cost = c(conservation = 5, restoration = 12)
)
print(p1)
#> A multiscape object (<Problem>)
#> ├─data
#> │├─planning units: <data.frame> (4 total)
#> │├─costs: min: 1, max: 4
#> │└─features: 2 total ("sp1", "sp2")
#> └─actions and effects
#> │├─actions: 2 total ("Conservation", "Restoration")
#> │├─feasible action pairs: 8 feasible rows
#> │├─action costs: min: 5, max: 12
#> │├─effect data: none
#> │└─profit data: none
#> └─spatial
#> │├─geometry: none
#> │├─coordinates: none
#> │└─relations: none
#> └─targets and constraints
#> │├─targets: none
#> │├─area constraints: none
#> │├─budget constraints: none
#> │├─planning-unit locks: none
#> │└─action locks: none
#> └─model
#> │├─status: not built yet (will build in solve())
#> │├─objectives: none
#> │├─method: single-objective
#> │├─solver: not set (auto)
#> │└─checks: incomplete (no objective registered)
#> # ℹ Use `x$data` to inspect stored tables and model snapshots.
utils::head(p1$data$dist_actions)
#> pu action cost status internal_pu internal_action
#> 1 1 conservation 5 0 1 1
#> 5 1 restoration 12 0 1 2
#> 2 2 conservation 5 0 2 1
#> 6 2 restoration 12 0 2 2
#> 3 3 conservation 5 0 3 1
#> 7 3 restoration 12 0 3 2
# Example 2: specify feasible pairs explicitly
include_df <- data.frame(
pu = c(1, 2, 3, 4),
action = c("conservation", "conservation", "restoration", "restoration")
)
p2 <- add_actions(
x = p,
actions = actions,
include_pairs = include_df,
cost = 10
)
p2$data$dist_actions
#> pu action cost status internal_pu internal_action
#> 1 1 conservation 10 0 1 1
#> 2 2 conservation 10 0 2 1
#> 3 3 restoration 10 0 3 2
#> 4 4 restoration 10 0 4 2
# Example 3: remove selected pairs after full expansion
exclude_df <- data.frame(
pu = c(2, 4),
action = c("restoration", "conservation")
)
p3 <- add_actions(
x = p,
actions = actions,
exclude_pairs = exclude_df,
cost = c(conservation = 3, restoration = 8)
)
p3$data$dist_actions
#> pu action cost status internal_pu internal_action
#> 1 1 conservation 3 0 1 1
#> 5 1 restoration 8 0 1 2
#> 2 2 conservation 3 0 2 1
#> 3 3 conservation 3 0 3 1
#> 7 3 restoration 8 0 3 2
#> 8 4 restoration 8 0 4 2
