--- title: "Joint effects and interactions between management actions" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Joint effects and interactions between management actions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(multiscape) ``` Supply the **total effect of executing a combination** in the same call as its individual effects. The package preserves those totals and calculates signed interaction corrections, rather than attributing portions to individual actions. ```{r inputs} base <- create_problem( data.frame(id = 10L, cost = 0), data.frame(id = 1L, name = "habitat"), data.frame(pu = 10L, feature = 1L, amount = 100) ) |> add_actions(data.frame(id = c("restore", "control")), cost = 1) |> add_action_sets(data.frame(set = c("restore_control", "restore_control"), action = c("restore", "control"))) |> add_constraint_action_cardinality(2, "max") effects <- data.frame(action = c("restore", "control", "restore_control"), feature = "habitat", effect = c(30, 20, 70)) problem <- add_effects(base, effects) problem$data$effect_terms[, c("action", "total_effect", "effect")] ``` The pair correction is `70 - 30 - 20 = 20`. Selecting both yields 70, while restoration alone yields 30. Registration and effects do not force selection; cardinality controls capacity and logical relations control permitted groups. The modern `action` field accepts atomic IDs or registered set IDs. `effect`, `outcome`, and `relative_change` keep their meanings, sharing the same reference: ```{r equivalent-inputs} outcomes <- data.frame(action = effects$action, feature = "habitat", outcome = c(130, 120, 170)) changes <- data.frame(action = effects$action, feature = "habitat", relative_change = c(.3, .2, .7)) outcome_problem <- add_effects(base, outcomes) relative_problem <- add_effects(base, changes) stopifnot(isTRUE(all.equal(problem$data$effect_terms, outcome_problem$data$effect_terms)), isTRUE(all.equal(problem$data$effect_terms, relative_problem$data$effect_terms))) ``` Modern raster lists can likewise include names of registered sets, with the same layer order, aggregation, reference, and interpretation rules as individual rasters. Legacy component filtering is rejected for sets. With no `pu`, a joint row expands only over units where every member has an available pair. Explicit joint rows with unavailable members are rejected. Later locks or incompatible constraints can make a supplied combination unreachable without removing its original observation. ## Preserve inputs and record assumptions ```{r provenance} problem$data$effects_input problem$data$joint_effects[, c("action", "effect", "action_outcome")] problem$data$effects_meta[c("missing_interactions", "missing_individual_effects")] ``` `effects_input` preserves the original table; `effects_original` preserves all canonical totals; `joint_effects` contains supplied set totals; `effect_terms` contains signed corrections. `dist_effects` retains individual rows for compatibility. Different set names cannot provide duplicate membership totals for one PU/feature. Zero corrections remain preserved as input observations. Unspecified individual effects and interactions are **assumed zero**. This is an explicit model assumption, not evidence that an unobserved interaction is absent. For three actions, subtract individual and supplied pair corrections: $$\delta_{ABC}=E(ABC)-E(A)-E(B)-E(C)-\delta_{AB}-\delta_{AC}-\delta_{BC}.$$ The rule extends to larger sets. No unprovided subsets are generated automatically. ## Exact activation and model size For set $S$, compilation adds a continuous auxiliary satisfying: $$0\le y_{i,S}\le1,\quad y_{i,S}\le x_{i,a}\ (a\in S),\quad y_{i,S}\ge\sum_{a\in S}x_{i,a}-|S|+1.$$ Binary member decisions determine it exactly, even with negative coefficients or economic objectives. One PU/membership variable is shared across features. Only reachable, non-zero corrections need auxiliaries. Availability, known cardinality limits, and exclusions can eliminate combinations. Inferred negative final feature amounts are excluded through linear outcome constraints. Ten actions have 1,013 subsets of size two or greater, but only non-zero, reachable corrections contribute variables. Sparse interactions limit growth; solution time still depends on the full model and the number of MO runs. ## Solve with all three MO methods The benefit objective maximizes signed change, including all individual contributions and the pair correction. Each method selects both actions, with benefit 70 and action cost 2. ```{r methods} mo_base <- problem |> add_constraint_action_cardinality(1, "min") |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_solver_cbc(gap_limit = 0, verbose = FALSE) methods <- list( weighted = set_method_weighted_sum(mo_base, aliases = c("cost", "benefit"), normalize_weights = FALSE, runs = set_runs_manual(data.frame(weight_cost = 1, weight_benefit = 1))), epsilon = set_method_epsilon_constraint(mo_base, primary = "cost", aliases = c("cost", "benefit"), runs = set_runs_manual(data.frame(eps_benefit = 69))), augmecon = set_method_augmecon(mo_base, primary = "cost", aliases = c("cost", "benefit"), runs = set_runs_manual(data.frame(eps_benefit = 69))) ) ``` ```{r solve-mo, eval=requireNamespace("rcbc", quietly = TRUE)} solutions <- lapply(methods, solve) lapply(solutions, get_objectives) for (solution in solutions) { objective <- get_objectives(solution) feature <- get_features(solution) stopifnot(objective$cost == 2, objective$benefit == 70, feature$selected_baseline == 100, feature$selected_amount_after == 170) } ``` ## Negative interactions, net benefit, and final loss ```{r negative-interaction} negative_input <- effects negative_input$effect[negative_input$action == "restore_control"] <- 40 negative <- add_effects(base, negative_input) negative$data$effect_terms[, c("action", "total_effect", "effect")] economic <- negative |> add_profit(c(restore = 10, control = 9)) |> add_objective_max_profit(alias = "profit") |> set_solver_cbc(gap_limit = 0, verbose = FALSE) ``` ```{r solve-negative, eval=requireNamespace("rcbc", quietly = TRUE)} solution <- solve(economic) get_features(solution) stopifnot(get_features(solution)$selected_net == 40, get_features(solution)$selected_benefit == 40, get_features(solution)$selected_loss == 0, get_features(solution)$selected_amount_after == 140) ``` The correction is -10, but the final change is +40. The summary reports benefit 40 and loss zero, counting the selected reference once. It does not classify the negative correction as an ecological loss by itself. `add_objective_max_effect()` now optimizes that signed net change. Earlier versions counted positive effects only; models with negative effects can change their solutions. No separate net-benefit function is needed. ```{r signed-objectives} benefit_problem <- negative |> add_objective_max_effect(alias = "benefit") loss_problem <- negative |> add_constraint_action_cardinality(2, "equal") |> add_objective_min_loss(alias = "loss") ``` ```{r solve-signed-objectives, eval=requireNamespace("rcbc", quietly = TRUE)} benefit_solution <- solve(set_solver_cbc(benefit_problem, gap_limit = 0, verbose = FALSE)) loss_solution <- solve(set_solver_cbc(loss_problem, gap_limit = 0, verbose = FALSE)) stopifnot(get_objectives(benefit_solution)$benefit == 40, get_objectives(loss_solution)$loss == 0) ``` For signed change $\Delta_{if}$ after combining scoped actions, the two criteria are $\max \sum_{i,f}\Delta_{if}$ and $\min \sum_{i,f}\max(-\Delta_{if},0)$. Gains can offset deterioration in the benefit objective. In the loss objective, gains in one unit or feature cannot cancel losses in another. Both retain independent action and feature scopes. An interaction belongs to an action scope only if every member belongs to it. Loss is linearized exactly using finite bounds derived from the signed coefficients. Mixed-sign PU/feature expressions require one continuous loss variable and one binary sign variable; sign-compatible expressions need no such auxiliaries. The constraints make the loss exact even if it is a secondary objective or has zero weight. All required scoped expressions are prepared in the common model used by weighted-sum, epsilon-constraint, and AUGMECON. ```{r deteriorating-joint-outcome} deteriorating_input <- effects deteriorating_input$effect[deteriorating_input$action == "restore_control"] <- -10 deteriorating <- add_effects(base, deteriorating_input) |> add_constraint_action_cardinality(2, "equal") ``` ```{r solve-deterioration, eval=requireNamespace("rcbc", quietly = TRUE)} net_solution <- deteriorating |> add_objective_max_effect(alias = "benefit") |> set_solver_cbc(gap_limit = 0, verbose = FALSE) |> solve() loss_solution <- deteriorating |> add_objective_min_loss(alias = "loss") |> set_solver_cbc(gap_limit = 0, verbose = FALSE) |> solve() stopifnot(get_objectives(net_solution)$benefit == -10, get_objectives(loss_solution)$loss == 10, get_features(loss_solution)$selected_amount_after == 90) ``` The pair correction is now -60; the objective counts the final change of -10, and the final loss is 10. It does not count the correction's magnitude as loss. ## Targets on joint outcomes Targets require the reference plus the joint change. With reference 100 and joint change +40, selecting both actions contributes 140, rather than adding the two individual post-action amounts (130 + 120). Action-scoped targets count the reference only when at least one scoped action is selected in the unit. ```{r joint-target} target_problem <- negative |> add_constraint_targets_absolute(140) |> add_objective_min_cost(alias = "cost") ``` ```{r solve-joint-target, eval=requireNamespace("rcbc", quietly = TRUE)} target_solution <- solve(set_solver_cbc(target_problem, gap_limit = 0, verbose = FALSE)) get_targets(target_solution) stopifnot(get_objectives(target_solution)$cost == 2, get_features(target_solution)$selected_amount_after == 140, get_targets(target_solution)$achieved == 140) ```