--- title: "Planning with concurrent management actions" output: rmarkdown::html_vignette: toc: true toc_depth: 2 vignette: > %\VignetteIndexEntry{Planning with concurrent management actions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(multiscape) can_solve <- requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) ``` A planning unit can support more than one management intervention. For example, restoration may be combined with threat control, whereas two alternative uses may be incompatible. **Concurrent-action planning** represents each feasible planning-unit--action pair as an individual decision, then specifies which combinations are allowed and how their effects are evaluated. This vignette brings together three complementary parts of `multiscape`: | Question | Function | What it changes | |:--|:--|:--| | Which combinations do we want to identify? | `add_action_sets()` | Registers memberships, not decisions or constraints | | How many actions can be selected in a unit? | `add_constraint_action_cardinality()` | Minimum, maximum, or exact counts | | Which actions depend on or exclude others? | `add_constraint_action_requires()`, `add_constraint_action_excludes()`, `add_constraint_action_together()` | Logical compatibility within each unit | **A registered action set is not an additional selectable action.** Actions remain the individual decisions. Moreover, registering a set does not require its members to be implemented together. This distinction becomes especially important when describing joint ecological effects with `add_effects()`. ## 1. Start from one spatial landscape We use the 64-unit grid supplied with `load_sim_multiaction()`, including its planning-unit geometries and two ecological features. To illustrate the action rules, we define four hypothetical interventions and spatially varying **economic returns**. These returns are teaching assumptions, not observations or ecological estimates from the simulated dataset. ```{r build-problem} sim <- load_sim_multiaction() # A common catalogue: each action is a separate binary decision in a unit. actions <- data.frame( id = c("restore", "control", "fence", "monitor") ) # Two illustrative opportunity areas in the 8-by-8 landscape. southwest <- with(sim$planning_units, x < 4 & y < 4) northeast <- with(sim$planning_units, x > 4 & y > 4) # Assign a return to every planning-unit--action pair. returns <- expand.grid( pu = sim$planning_units$id, action = actions$id, KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE ) idx <- match(returns$pu, sim$planning_units$id) returns$profit <- -3 # No intervention is preferred outside opportunity areas. returns$profit[returns$action == "restore" & southwest[idx]] <- 8 returns$profit[returns$action == "control" & southwest[idx]] <- -2 returns$profit[returns$action == "fence" & northeast[idx]] <- 7 returns$profit[returns$action == "monitor" & northeast[idx]] <- 4 # Build the problem without any special action-selection rules. raw <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(actions, cost = 1) |> add_profit(returns) ``` The spatial patterns make the examples interpretable: restoration has a positive return in the south-west, while its companion threat-control action has a negative return. Fencing and monitoring have positive returns in the north-east. Outside these areas all returns are negative. The subsequent gross-profit objective **does not deduct implementation costs**; we use those costs only to illustrate that costs and returns are distinct inputs. ## 2. Name combinations with action sets Action sets identify combinations of *existing* actions. A member may occur in more than one set, but sets do not contain other sets. ```{r register-sets} base <- raw |> add_action_sets(list( restore_control = c("restore", "control"), restore_monitor = c("restore", "monitor"), fence_monitor = c("fence", "monitor") )) # Membership is a compact table; no new decision variable has been added. get_action_sets(base) ``` The equivalent long-table input has one row per membership: ```{r alternative-set-format} memberships <- data.frame( set = c("restore_control", "restore_control", "restore_monitor", "restore_monitor", "fence_monitor", "fence_monitor"), action = c("restore", "control", "restore", "monitor", "fence", "monitor") ) alternative <- add_action_sets(raw, memberships) ``` Both formats describe the same memberships. Here, `restore` belongs to two sets, illustrating that membership is many-to-many. Register sets *after* registering their individual member actions. Each set must contain at least two distinct members, and its identifier cannot duplicate an action identifier. Reusing a set identifier or membership is rejected. Crucially, **`base` still allows at most one action per planning unit**. Merely registering `restore_control` does not permit or enforce concurrent selection. ## 3. Change the number of actions allowed in each unit The default is zero or one selected action per unit. To permit concurrent interventions, set a higher total maximum: ```{r permit-concurrency} concurrent <- base |> add_constraint_action_cardinality(2, "max", name = "two_per_unit") ``` `count` applies **separately to each unit**, not to all units combined. The three available senses are: | `sense` | Interpretation in each scoped unit | |:--|:--| | `"min"` | Select at least `count` actions | | `"max"` | Select at most `count` actions | | `"equal"` | Select exactly `count` actions | More specific limits may cover different units or action subsets: ```{r cardinality-variations} # Up to two actions in the south-west, but the default one elsewhere. local_capacity <- base |> add_constraint_action_cardinality( 2, "max", pu = sim$planning_units$id[southwest] ) # Allow two in total, but at most one of restoration or control. subset_capacity <- base |> add_constraint_action_cardinality(2, "max") |> add_constraint_action_cardinality( 1, "max", actions = c("restore", "control") ) # Exactly two actions in each south-west unit. exact_two <- base |> add_constraint_action_cardinality( 2, "equal", pu = sim$planning_units$id[southwest] ) ``` An explicit **total** maximum or equality overrides the implicit one-action limit for the units it covers. A lower bound alone, or a bound restricted to an action subset, does not. Thus, to require at least two actions, first permit two or more with a total maximum. Distinct rules accumulate; a subsequent rule does not erase an earlier one. ## 4. Specify dependencies and compatibility Allowing two actions does not mean that *every* combination is appropriate. Three functions encode the relevant logical relationships: - `add_constraint_action_requires()`: a selected trigger needs its companion(s); `sense = "all"` requires every companion and `sense = "any"` requires at least one. - `add_constraint_action_excludes()`: at most one action from the specified group may be selected. - `add_constraint_action_together()`: all actions in the group must be selected together, or all must remain unselected. Consider only `restore` (R) and `control` (C). The table shows combinations that each *individual* rule permits: | Rule | Neither | R only | C only | R + C | |:--|:--:|:--:|:--:|:--:| | Default maximum 1 | Yes | Yes | Yes | No | | Total maximum 2 | Yes | Yes | Yes | Yes | | Maximum 2 + R requires C | Yes | No | Yes | Yes | | Maximum 2 + R excludes C | Yes | Yes | Yes | No | | Maximum 2 + R together with C | Yes | No | No | Yes | All three relations are **within-unit** rules: a companion selected in one unit cannot fulfil a requirement in another. None of these rules forces its actions to be selected unless another constraint or a fixed decision does so. ### Directional requirements and alternatives ```{r requirements} # Whenever restoration is chosen in a south-west unit, control is required. dependent <- concurrent |> add_constraint_action_requires( actions = "restore", requires = "control", pu = sim$planning_units$id[southwest] ) # A different example: either control or monitoring can accompany restoration. flexible <- concurrent |> add_constraint_action_requires( actions = "restore", requires = c("control", "monitor"), sense = "any", pu = sim$planning_units$id[southwest] ) ``` A requirement is **directional**: choosing control alone does not require restoration. If `actions` contains several triggers, *each* trigger is checked independently. For `sense = "any"`, choosing both companions is also allowed, provided cardinality permits it. ### Exclusions and selection together ```{r exclusions-and-together} # Fencing and monitoring cannot be combined in north-east units. exclusive <- concurrent |> add_constraint_action_excludes( c("fence", "monitor"), pu = sim$planning_units$id[northeast] ) # Reuse the MEMBERS of a registered set, not the set identifier itself. registered <- get_action_sets(base) members <- registered$action[registered$set == "restore_control"] joint_selection <- concurrent |> add_constraint_action_together( members, pu = sim$planning_units$id[southwest] ) ``` `excludes` applies pairwise even if a group contains more than two actions. `together` enforces both directions of a dependency; it does not itself require that the group be selected. The action-set identifier `"restore_control"` is **not** a selectable action and therefore cannot be supplied in place of `members` to these constraint functions. ## 5. Compare three spatial prescriptions We now solve three versions of **the same economic problem**, differing only in the action-selection rules. We maximise gross profit to make the effect of those rules transparent; this is not a multi-objective example. ```{r configure-scenarios} # A: one action at most (the package default). single <- base # B: up to two actions in every unit. # Positive-return actions may now be combined where advantageous. # C: two actions allowed, but restoration needs control in the south-west; # fencing and monitoring are incompatible in the north-east. compatible <- concurrent |> add_constraint_action_requires( "restore", "control", pu = sim$planning_units$id[southwest] ) |> add_constraint_action_excludes( c("fence", "monitor"), pu = sim$planning_units$id[northeast] ) models <- list( single = single, concurrent = concurrent, compatible = compatible ) ``` ```{r solve-scenarios, eval=can_solve} solutions <- lapply(models, function(problem) { problem |> add_objective_max_profit(alias = "profit") |> set_solver_cbc(time_limit = 30, verbose = FALSE) |> solve() }) # A concise comparison: objective values and selected action counts. comparison <- do.call(rbind, lapply(names(solutions), function(label) { result <- solutions[[label]] selected <- get_actions(result, solution = 1) selected <- selected[selected$selected > 0.5, , drop = FALSE] counts <- table(factor(selected$action, levels = actions$id)) data.frame( scenario = label, profit = get_objectives(result, format = "wide")$profit[1], restore = unname(counts["restore"]), control = unname(counts["control"]), fence = unname(counts["fence"]), monitor = unname(counts["monitor"]), row.names = NULL ) })) comparison ``` The examples were constructed so the economic incentives are easy to follow. The default selects restoration in the south-west and fencing in the north-east. Permitting two actions adds monitoring alongside fencing where both have positive returns. Under the compatibility rules, restoration must also bring the otherwise unprofitable control action in the south-west, while fencing and monitoring cannot coexist in the north-east. Thus the three scenarios produce distinct action patterns. Unfavourable units remain unmanaged. With 16 units in each opportunity area, the expected gross-profit values are **240** (single), **304** (concurrent), and **208** (compatible). These values can be checked against the table above. The following maps are generated directly by `plot_spatial_actions()`. Its `layout = "single"` mode displays concurrent selections with combined labels such as `restore+control`; **do not wrap this plotting function in `print()`**. ```{r map-single, eval=can_solve, fig.width=7.5, fig.height=4.8, fig.cap="A. Default: at most one action in each planning unit."} plot_spatial_actions(solutions$single, layout = "single") ``` ```{r map-concurrent, eval=can_solve, fig.width=7.5, fig.height=4.8, fig.cap="B. Up to two actions: concurrent selections become possible."} plot_spatial_actions(solutions$concurrent, layout = "single") ``` ```{r map-compatible, eval=can_solve, fig.width=7.5, fig.height=4.8, fig.cap="C. Action dependencies and exclusions restrict which concurrent choices are possible."} plot_spatial_actions(solutions$compatible, layout = "single") ``` If the optional `rcbc` or `ggplot2` package is not installed, the models are still configured but the solver output and figures are skipped when building this vignette. ## 6. Distinguish selecting a combination from describing its joint effect So far, the objective used independent **economic returns**. A registered set can also identify a combination whose **ecological effect differs from the sum of its individual effects**. For example, suppose restoration contributes a change of 0.10 in woodland and control contributes 0.05. Together, their hypothesised *total* change is 0.20 rather than 0.15. The additional 0.05 is an interaction, but the value registered for the set is the **total**, not the additional interaction: ```{r joint-outcome} joint_effects <- concurrent |> add_effects(data.frame( action = c("restore", "control", "restore_control"), feature = "woodland", effect = c(0.10, 0.05, 0.20) )) ``` This use of `"restore_control"` is different from supplying the set identifier to a selection constraint. `add_effects()` uses it to describe the outcome when both member actions are selected. By itself, `add_effects()` does not compel them to be selected. Conversely, `add_constraint_action_together()` imposes joint selection but does **not** specify an ecological interaction. For the representation of additive and non-additive effects, multiple features, and interaction corrections, continue with [Joint effects and interactions between management actions](Joint_effects.html). ## 7. Practical rules and common pitfalls - **Scope.** `pu = NULL` means all currently registered units; `pu = c(10, 20)` applies a relation separately to each unit, not once across their union. - **Implicit capacity.** A subset minimum or maximum does not remove the default overall capacity of one action per unit. To allow a companion, first increase total cardinality. - **Available pairs.** A missing or locked-out feasible pair is treated as an unavailable decision. A missing required companion can prohibit its trigger; a missing member of a `together` group can prohibit the whole group. - **Accumulating rules.** Distinct cardinality constraints and logical relations are enforced simultaneously. Duplicate definitions or duplicate names can be rejected; later calls do not overwrite earlier rules. - **Infeasibility.** Valid individual rules can conflict with one another, action locks, area requirements, or budgets. A higher cardinality maximum permits concurrency but never guarantees feasibility. - **Economic versus ecological effects.** Profit is the sum of selected action-specific returns, whereas `add_effects()` can account for non-additive ecological responses through registered combinations. A unit-level cost is charged once per selected unit; an action cost applies to each selected action when included in a cost-based objective. The central distinction is between **what may be selected** (cardinality and logical relations), **what a combination is called** (action sets), and **what happens when it is selected** (ecological effects, costs, and profit). See also [`add_actions()`](../reference/add_actions.html), [`add_constraint_locked_actions()`](../reference/add_constraint_locked_actions.html), and [`add_objective_max_profit()`](../reference/add_objective_max_profit.html).