Skip to contents

Define the effects of management actions on features across planning units.

Effects are stored in a canonical representation in a effects table, with one row per (pu, action, feature) triple and two non-negative columns:

  • benefit: the positive component of the effect,

  • loss: the magnitude of the negative component of the effect.

The net effect is therefore interpreted as $$ \Delta_{iaf} = \mathrm{benefit}_{iaf} - \mathrm{loss}_{iaf}, $$ where \(i\) indexes planning units, \(a\) indexes actions, and \(f\) indexes features.

Under the semantics adopted by this package, each (pu, action, feature) triple represents a single net effect. Consequently, after validation and aggregation, a stored row cannot have both benefit > 0 and loss > 0 at the same time.

Usage

add_effects(
  x,
  effects = NULL,
  effect_type = c("delta", "after"),
  effect_aggregation = c("sum", "mean"),
  component = c("any", "benefit", "loss")
)

Arguments

x

A Problem object created with create_problem. It must already contain feasible actions; run add_actions first.

effects

Effect specification. One of:

  • NULL, to store an empty effects table,

  • a data.frame(action, feature, multiplier),

  • a data.frame(pu, action, feature, ...) with explicit effects,

  • a named list of terra::SpatRaster objects, one per action.

effect_type

Character string indicating how supplied effect values are interpreted. Must be one of:

  • "delta": values represent signed net changes,

  • "after": values represent after-action amounts and are converted to net changes relative to baseline feature amounts.

effect_aggregation

Character string giving the aggregation used when converting raster values to planning-unit level. Must be one of "sum" or "mean".

component

Character string controlling which component of the canonical effects table is retained. Must be one of:

  • "any": keep all non-zero rows,

  • "benefit": keep only rows with benefit > 0,

  • "loss": keep only rows with loss > 0.

Value

An updated Problem object containing:

dist_effects

A canonical effects table with columns pu, action, feature, benefit, loss, internal_pu, internal_action, internal_feature, and optional labels such as feature_name and action_name.

effects_meta

Metadata describing how effects were interpreted and stored.

Details

When to use add_effects().

Use this function when you want to specify what feasible actions do to features. It is the stage at which an action-based decision space is linked to feature-level ecological or functional consequences.

This function provides a unified interface for specifying action effects from several input formats while enforcing a single internal representation. Regardless of how the user supplies the effects, the stored output always follows the same canonical structure based on non-negative benefit/loss components.

Let \(i \in \mathcal{I}\) index planning units, \(a \in \mathcal{A}\) index actions, and \(f \in \mathcal{F}\) index features. Let \(b_{if}\) denote the baseline amount of feature \(f\) in planning unit \(i\), as given by the feature-distribution table. Let \(\Delta_{iaf}\) denote the net change caused by applying action \(a\) in planning unit \(i\) to feature \(f\). The canonical stored representation is:

$$ \mathrm{benefit}_{iaf} = \max(\Delta_{iaf}, 0), $$

$$ \mathrm{loss}_{iaf} = \max(-\Delta_{iaf}, 0). $$

Hence:

  • if \(\Delta_{iaf} > 0\), then benefit > 0 and loss = 0,

  • if \(\Delta_{iaf} < 0\), then benefit = 0 and loss > 0,

  • if \(\Delta_{iaf} = 0\), then both are zero.

Why split effects into benefit and loss?

This representation avoids ambiguity in downstream optimization models. It allows the package to support, for example, objectives that maximize beneficial effects, minimize damages, impose no-net-loss conditions, or combine both components differently in multi-objective formulations.

Supported effect specifications

The effects argument may be provided in one of the following forms:

  1. NULL. An empty effects table is stored.

  2. A data.frame(action, feature, multiplier). In this case, effects are constructed by multiplying baseline feature amounts by the supplied multiplier: $$ \Delta_{iaf} = b_{if} \times m_{af}, $$ where \(m_{af}\) is the multiplier associated with action \(a\) and feature \(f\). This specification is expanded over all feasible (pu, action) pairs.

  3. A data.frame(pu, action, feature, ...) giving explicit effects for individual triples. The table may contain:

    • delta or effect: interpreted as signed net changes,

    • after: interpreted as after-action amounts if effect_type = "after",

    • benefit and/or loss: explicit non-negative split components,

    • legacy signed benefit without loss: interpreted as a signed net effect for backwards compatibility.

  4. A named list of terra::SpatRaster objects, one per action. In this case, names must match action ids, and each raster must contain one layer per feature. Raster values are aggregated to planning-unit level using effect_aggregation.

Interpretation of effect_type

If effect_type = "delta", supplied values are interpreted as net changes directly.

If effect_type = "after", supplied values are interpreted as after-action amounts and converted internally to net effects using:

$$ \Delta_{iaf} = \mathrm{after}_{iaf} - b_{if}. $$

Missing baseline values are treated as zero.

Feasibility and locked-out decisions

Effects are only retained for feasible (pu, action) pairs. Thus, add_actions() must be called first. Pairs marked as locked out (status == 3) are removed before storing the final effects table.

This function does not define the action-decision layer itself; it builds on the feasible (pu, action) pairs already stored in the problem.

Duplicate rows and semantic validation

If multiple rows are supplied for the same (pu, action, feature) triple, they are aggregated by summing benefit and loss separately. The resulting triple must still respect the package semantics, namely that both components cannot be strictly positive simultaneously. Inputs violating this rule are rejected.

Component filtering

After canonicalization and validation, rows can be restricted to:

  • component = "any": keep all non-zero effects,

  • component = "benefit": keep only rows with benefit > 0,

  • component = "loss": keep only rows with loss > 0.

Rows with benefit = 0 and loss = 0 are always removed.

Raster handling

When effects are supplied as rasters, they are automatically aligned to the planning-unit raster or geometry when needed before extraction or zonal aggregation.

Stored output

The resulting effects table contains user-facing ids, internal integer ids, and optional labels for actions and features. Metadata describing the stored representation and input interpretation are written to an effects metadata field.

After defining effects, typical next steps include adding objectives that use beneficial or harmful effects, and then solving the configured problem.

Examples

# ------------------------------------------------------
# Minimal problem with actions
# ------------------------------------------------------
pu <- data.frame(
  id = 1:3,
  cost = c(1, 2, 3)
)

features <- data.frame(
  id = 1:2,
  name = c("sp1", "sp2")
)

dist_features <- data.frame(
  pu = c(1, 1, 2, 3),
  feature = c(1, 2, 1, 2),
  amount = c(10, 5, 8, 4)
)

p <- create_problem(
  pu = pu,
  features = features,
  dist_features = dist_features
)

actions <- data.frame(
  id = c("conservation", "restoration"),
  name = c("Conservation", "Restoration")
)

p <- add_actions(
  x = p,
  actions = actions,
  cost = c(conservation = 2, restoration = 4)
)

print(p)
#> A multiscape object (<Problem>)
#> ├─data
#> │├─planning units: <data.frame> (3 total)
#> │├─costs: min: 1, max: 3
#> │└─features: 2 total ("sp1", "sp2")
#> └─actions and effects
#> │├─actions: 2 total ("Conservation", "Restoration")
#> │├─feasible action pairs: 6 feasible rows
#> │├─action costs: min: 2, max: 4
#> │├─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.

# ------------------------------------------------------
# 1) Empty effects
# ------------------------------------------------------
p0 <- add_effects(p, effects = NULL)
#> Warning: All effect values are zero after filtering.
p0$data$dist_effects
#>  [1] pu               action           feature          benefit         
#>  [5] loss             internal_pu      internal_action  internal_feature
#>  [9] feature_name     action_name     
#> <0 rows> (or 0-length row.names)

# ------------------------------------------------------
# 2) Multipliers by action and feature
# delta = baseline amount * multiplier
# ------------------------------------------------------
mult <- data.frame(
  action = c("conservation", "conservation",
             "restoration", "restoration"),
  feature = c("sp1", "sp2", "sp1", "sp2"),
  multiplier = c(0.10, 0.00, -0.20, 0.25)
)

p1 <- add_effects(
  x = p,
  effects = mult,
  effect_type = "delta"
)

p1$data$dist_effects
#>   pu       action feature benefit loss internal_pu internal_action
#> 1  1 conservation       1    1.00  0.0           1               1
#> 2  2 conservation       1    0.80  0.0           2               1
#> 3  1  restoration       1    0.00  2.0           1               2
#> 4  2  restoration       1    0.00  1.6           2               2
#> 7  1  restoration       2    1.25  0.0           1               2
#> 8  3  restoration       2    1.00  0.0           3               2
#>   internal_feature feature_name  action_name
#> 1                1          sp1 Conservation
#> 2                1          sp1 Conservation
#> 3                1          sp1  Restoration
#> 4                1          sp1  Restoration
#> 7                2          sp2  Restoration
#> 8                2          sp2  Restoration

# ------------------------------------------------------
# 3) Explicit net effects using signed deltas
# ------------------------------------------------------
eff_delta <- data.frame(
  pu = c(1, 2, 3, 3),
  action = c("conservation", "restoration", "restoration", "conservation"),
  feature = c(1, 1, 2, 2),
  delta = c(2, -3, 1, 0.5)
)

p2 <- add_effects(
  x = p,
  effects = eff_delta
)

p2$data$dist_effects
#>   pu       action feature benefit loss internal_pu internal_action
#> 1  1 conservation       1     2.0    0           1               1
#> 2  2  restoration       1     0.0    3           2               2
#> 3  3 conservation       2     0.5    0           3               1
#> 4  3  restoration       2     1.0    0           3               2
#>   internal_feature feature_name  action_name
#> 1                1          sp1 Conservation
#> 2                1          sp1  Restoration
#> 3                2          sp2 Conservation
#> 4                2          sp2  Restoration

# ------------------------------------------------------
# 4) Explicit after-action amounts
# delta = after - baseline
# ------------------------------------------------------
eff_after <- data.frame(
  pu = c(1, 2, 3),
  action = c("conservation", "restoration", "restoration"),
  feature = c(1, 1, 2),
  after = c(12, 5, 6)
)

p3 <- add_effects(
  x = p,
  effects = eff_after,
  effect_type = "after"
)

p3$data$dist_effects
#>   pu       action feature benefit loss internal_pu internal_action
#> 1  1 conservation       1       2    0           1               1
#> 2  2  restoration       1       0    3           2               2
#> 3  3  restoration       2       2    0           3               2
#>   internal_feature feature_name  action_name
#> 1                1          sp1 Conservation
#> 2                1          sp1  Restoration
#> 3                2          sp2  Restoration

# ------------------------------------------------------
# 5) Keep only beneficial effects
# ------------------------------------------------------
p4 <- add_effects(
  x = p,
  effects = eff_delta,
  component = "benefit"
)

p4$data$dist_effects
#>   pu       action feature benefit loss internal_pu internal_action
#> 1  1 conservation       1     2.0    0           1               1
#> 3  3 conservation       2     0.5    0           3               1
#> 4  3  restoration       2     1.0    0           3               2
#>   internal_feature feature_name  action_name
#> 1                1          sp1 Conservation
#> 3                2          sp2 Conservation
#> 4                2          sp2  Restoration