| Title: | Multi-Action, Multi-Objective Spatial Planning |
|---|---|
| Description: | Provides a modular framework for exact multi-objective spatial planning using mixed-integer programming. The package supports the definition of planning problems through planning units, features, management actions, action effects, spatial relations, targets, constraints, and objective functions. It enables the optimisation of spatial planning portfolios under considerations such as boundary structure, connectivity, and fragmentation. Supported multi-objective methods include weighted-sum aggregation, epsilon-constraint, and the augmented epsilon-constraint method. Problems can be solved with several commercial and open-source optimisation solvers. Optional solver backends include the 'gurobi' R package, which is distributed with the Gurobi Optimizer installation <https://docs.gurobi.com/projects/optimizer/en/13.0/reference/r/setup.html>, and the 'rcbc' R package, available from GitHub at <https://github.com/dirkschumacher/rcbc>. For background on multi-objective optimisation methods, see Halffmann et al. (2022) <doi:10.1002/mcda.1780>; for the augmented epsilon-constraint method, see Mavrotas (2009) <doi:10.1016/j.amc.2009.03.037>. |
| Authors: | José Salgado-Rojas [aut, cre] (ORCID: <https://orcid.org/0000-0001-9817-2625>), Matías Moreno-Faguett [aut] (ORCID: <https://orcid.org/0009-0003-6561-234X>), Victoria Madrid-Ayala [aut] (ORCID: <https://orcid.org/0009-0008-5678-3418>), Sara Pineda-Zapata [aut] (ORCID: <https://orcid.org/0009-0006-8014-0098>), Virgilio Hermoso [aut] (ORCID: <https://orcid.org/0000-0003-3205-5033>), Núria Aquilué [aut] (ORCID: <https://orcid.org/0000-0001-7911-3144>) |
| Maintainer: | José Salgado-Rojas <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 1.4.4 |
| Built: | 2026-10-26 13:48:15 UTC |
| Source: | https://github.com/josesalgr/multiscape |
Define named combinations of existing actions. An action can belong to more than one set. Sets are stored separately from the individual action catalog.
add_action_sets(x, sets)add_action_sets(x, sets)
x |
A |
sets |
A non-empty named list of action-id vectors, or a non-empty
|
Run add_actions() first. Supply either a named list of action-id vectors or
a long table with one row per membership and columns set and action.
Additional table columns are ignored. Each set must contain at least two
distinct registered actions; duplicate memberships and nested sets are
rejected. Members are matched by action id, not by their display name.
Set identifiers cannot coincide with action ids, action display names, or
existing labels in actions$action_set. The latter column remains an
independent, backward-compatible action classification.
Repeated calls add new sets. Reusing a registered set identifier is an error,
including attempts to extend or redefine its membership. Definitions are
returned in a consistent order by get_action_sets().
Registering a set does not create a selectable action, change feasible
planning-unit/action pairs, require its members to be selected together, or
add an interaction effect. It also does not enable simultaneous actions:
the default maximum of one selected action per planning unit still applies
unless changed with add_constraint_action_cardinality().
Sets may be registered even if their members have no common feasible unit.
The modern add_effects() interface accepts these identifiers for supplied
total joint effects. Objectives and constraints still receive individual
members. Joint effects do not make a set a separate selectable action.
A new Problem object containing the definitions. The input object
is not modified. Any previously compiled model is invalidated, following
the usual problem-construction workflow.
add_actions(), get_action_sets(), add_constraint_action_cardinality()
problem <- create_problem( pu = data.frame(id = 1:2, cost = c(1, 2)), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame( pu = 1:2, feature = 1L, amount = c(10, 20) ) ) |> add_actions( actions = data.frame(id = c("restore", "control", "fence")), cost = 1 ) # An action may belong to several sets. list_problem <- problem |> add_action_sets(list( restore_control = c("restore", "control"), restore_fence = c("restore", "fence") )) get_action_sets(list_problem) # Equivalent definitions in long-table format. memberships <- data.frame( set = c("restore_control", "restore_control", "restore_fence", "restore_fence"), action = c("restore", "control", "restore", "fence") ) table_problem <- add_action_sets(problem, memberships) identical(get_action_sets(list_problem), get_action_sets(table_problem)) # Add another set without changing existing definitions. table_problem <- table_problem |> add_action_sets(list(control_fence = c("control", "fence"))) get_action_sets(table_problem)problem <- create_problem( pu = data.frame(id = 1:2, cost = c(1, 2)), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame( pu = 1:2, feature = 1L, amount = c(10, 20) ) ) |> add_actions( actions = data.frame(id = c("restore", "control", "fence")), cost = 1 ) # An action may belong to several sets. list_problem <- problem |> add_action_sets(list( restore_control = c("restore", "control"), restore_fence = c("restore", "fence") )) get_action_sets(list_problem) # Equivalent definitions in long-table format. memberships <- data.frame( set = c("restore_control", "restore_control", "restore_fence", "restore_fence"), action = c("restore", "control", "restore", "fence") ) table_problem <- add_action_sets(problem, memberships) identical(get_action_sets(list_problem), get_action_sets(table_problem)) # Add another set without changing existing definitions. table_problem <- table_problem |> add_action_sets(list(control_fence = c("control", "fence"))) get_action_sets(table_problem)
Define the action catalogue, the set of feasible planning unit–action pairs, their implementation costs, and the effective area represented by each feasible planning unit–action decision.
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, effective action areas,
status codes, and internal indices used by the optimization backend.
Conceptually, if is the set of planning units and
is the set of actions, this function determines which
pairs are feasible decisions
and assigns a non-negative implementation cost to each feasible pair.
add_actions( x, actions, include_pairs = NULL, exclude_pairs = NULL, cost = NULL, action_area = NULL )add_actions( x, actions, include_pairs = NULL, exclude_pairs = NULL, cost = NULL, action_area = NULL )
x |
A |
actions |
A |
include_pairs |
Optional specification of feasible |
exclude_pairs |
Optional specification of infeasible |
cost |
Optional cost specification for feasible pairs. It may be
|
action_area |
Optional effective area specification for feasible
|
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.
To register combinations with many-to-many membership, use
add_action_sets after defining the individual actions. Existing
definitions are validated whenever the action catalog is updated.
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
.
If include_pairs is supplied, only those pairs are retained. If
exclude_pairs is also supplied, matching pairs are removed
afterwards.
More precisely, let denote the set of
included planning unit–action pairs and let
denote the set of excluded pairs.
If include_pairs = NULL, the feasible decision set is:
If include_pairs is supplied, the feasible decision set is:
Both include_pairs and exclude_pairs can be specified as:
NULL,
a data.frame with columns pu and action,
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 sf object defining the spatial zone where the action is
feasible.
Lists may mix vectors of planning-unit ids and sf objects across
actions. In the spatial case, feasible planning units are identified using
sf::st_intersects() against the stored planning-unit geometry. If
include_pairs or exclude_pairs contains sf objects, the
problem must contain planning-unit geometry in x$data$pu_sf; otherwise
an error is raised.
Spatial exclusions are applied after inclusions. If a planning unit–action
pair is included and excluded, the exclusion takes precedence and the pair is
removed. Spatial exclusions remove complete (pu, action) pairs; they
do not partially subtract area from included geometries.
Action areas.
The action_area column in dist_actions stores the effective
area represented by each feasible (pu, action) decision. This is an
area, not an action intensity.
If action_area = NULL, action areas are derived automatically:
when include_pairs is supplied using sf objects, action
areas are computed as the area of the spatial intersection between
planning-unit geometries and the corresponding action geometries;
when feasible pairs are supplied without spatial geometries, action areas default to the full planning-unit area when this can be derived from the problem object.
If action_area is supplied as a data.frame, it must contain
columns pu, action, and action_area. A column named
area is also accepted and renamed internally to action_area.
Supplied areas must be finite and non-negative. User-supplied values override
automatically derived values for matching feasible pairs. Rows referring to
non-feasible pairs are ignored with a warning.
If full planning-unit areas cannot be derived for some feasible pairs,
action_area is left as NA for those pairs. Area-based action
constraints should check for missing action_area values before model
construction.
Feasibility versus decision fixing.
This function only determines whether a pair 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 of
1.
If cost is a scalar, that value is assigned to all feasible
pairs.
If cost is a named numeric vector, names must match action ids
and costs are assigned by action.
If cost is a data.frame, it must define either:
action-level costs through columns action and cost, or
pair-specific costs through columns pu, action, and
cost.
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.
Repeated calls.
The action catalogue and feasible pairs can be defined only once per problem. A second call raises an error, even if the input is identical. To compare action catalogues, build separate problems from the object before this call.
After defining actions, typical next steps include adding effects, optional
decision-fixing constraints, objectives, and solver settings before calling
solve().
An updated Problem object with:
actionsThe action catalogue, including a unique integer
internal_id for each action.
dist_actionsThe feasible planning unit–action table with
columns pu, action, cost, action_area,
status, internal_pu, and internal_action.
pu indexA mapping from user-supplied planning-unit ids to internal integer ids.
action indexA mapping from action ids to internal integer ids.
create_problem,
add_action_sets,
add_constraint_locked_actions
# 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" ) 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) utils::head(p1$data$dist_actions) # 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 # 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) ) utils::head(p3$data$dist_actions) # Example 4: provide action-specific areas manually action_area <- data.frame( pu = c(1, 2, 3, 4), action = c("conservation", "conservation", "restoration", "restoration"), action_area = c(100, 50, 80, 100) ) p4 <- add_actions( x = p, actions = actions, include_pairs = include_df, action_area = action_area, cost = 10 ) p4$data$dist_actions# 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" ) 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) utils::head(p1$data$dist_actions) # 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 # 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) ) utils::head(p3$data$dist_actions) # Example 4: provide action-specific areas manually action_area <- data.frame( pu = c(1, 2, 3, 4), action = c("conservation", "conservation", "restoration", "restoration"), action_area = c(100, 50, 80, 100) ) p4 <- add_actions( x = p, actions = actions, include_pairs = include_df, action_area = action_area, cost = 10 ) p4$data$dist_actions
Legacy convenience wrapper around add_effects that retains
only positive signed changes (effect > 0). Positive change does not
necessarily imply an ecological benefit. For new code, prefer
add_effects() to retain signed effects of both directions.
Effects can be defined only once per problem.
add_benefits( x, benefits = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean") )add_benefits( x, benefits = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean") )
x |
A |
benefits |
Alias of |
effect_type |
Deprecated interpretation argument: |
effect_aggregation |
Deprecated raster aggregation argument; use
|
An updated Problem object containing:
dist_effectsThe canonical filtered effects table, containing
only rows with benefit > 0.
dist_benefitA backwards-compatible mirror table containing only the benefit component.
add_effects,
add_losses,
add_objective_max_benefit
Set a minimum, maximum, or exact number of management actions that can be selected in each planning unit. Rules can apply to all units, selected units, all actions, or a specified subset of actions.
add_constraint_action_cardinality( x, count, sense, actions = NULL, pu = NULL, name = NULL )add_constraint_action_cardinality( x, count, sense, actions = NULL, pu = NULL, name = NULL )
x |
A |
count |
A single finite, non-negative integer giving the number of selected individual actions per planning unit. |
sense |
One of |
actions |
Optional subset of action identifiers or existing
|
pu |
Optional vector of planning-unit identifiers. |
name |
Optional name for the constraint. A unique name is generated
when |
How action cardinality works
A cardinality constraint limits the number of individual actions selected
within each planning unit. For example, count = 2 and
sense = "max" allow zero, one, or two actions per unit, whereas
sense = "equal" requires exactly two. The available senses are:
"max": select at most count actions.
"min": select at least count actions.
"equal": select exactly count actions.
The limit is evaluated separately for every unit covered by pu;
it is not a total number of actions across the study area.
Default and overlapping rules
By default, at most one action can be selected per planning unit. To allow
concurrent actions, specify a total maximum or exact-count rule with
actions = NULL. That rule replaces the default one-action limit only
in the planning units it covers. A minimum rule or a rule restricted to
an action subset does not remove the default limit.
For example, to require at least two actions in a unit, first allow at least two with a total maximum, then add the minimum. Rules from different calls are enforced together; later rules do not overwrite earlier ones. Conflicting rules can make the problem infeasible.
Restricting the scope
Supply pu to restrict a rule to particular planning units and
actions to count only specified actions. For example, a minimum
of one action from c("restore", "control") requires at least one
of those actions in every covered unit. Actions excluded by the subset
are not counted towards that particular rule.
Cardinality counts individual selected actions, not the action sets
registered with add_action_sets(). Registered sets describe joint
effects when their member actions are selected; they are not additional
selectable actions.
Feasibility and stored rules
Only available planning-unit/action pairs can be selected. A positive
minimum or exact count larger than the number of available actions in a
covered unit is rejected. Conflicts with other constraints may still be
detected only when the problem is solved. Explicit rules are stored in
x$data$constraints$action_cardinality and do not themselves
select actions or run an optimisation solver.
An updated Problem with an additional rule in
x$data$constraints$action_cardinality. The input problem is
preserved; no optimisation is performed.
add_actions, add_action_sets,
add_constraint_budget
# EXAMPLE 1: Define the available decisions # Consider three planning units, each with four possible actions. # By default, the model allows at most one action in each unit. # Registering actions does not select any of them. problem <- create_problem( pu = data.frame(id = c(10L, 20L, 30L), cost = 1), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame( pu = c(10L, 20L, 30L), feature = 1L, amount = 10 ) ) |> add_actions( data.frame(id = c("restore", "control", "fence", "monitor")), cost = 1 ) # EXAMPLE 2: Different capacities in different planning units # Allow up to three actions in unit 10 and up to two in unit 20. # Unit 30 retains the default maximum of one action. # A maximum permits fewer actions, including no intervention. capacities <- problem |> add_constraint_action_cardinality( count = 3, sense = "max", pu = 10L, name = "capacity_10" ) |> add_constraint_action_cardinality( count = 2, sense = "max", pu = 20L, name = "capacity_20" ) # Inspect the registered constraints, not solved decisions. capacities$data$constraints$action_cardinality[ , c("name", "count", "sense") ] # EXAMPLE 3: Require at least one action from a subset # In units 10 and 20, require restoration or control (or both). # The minimum applies separately to each unit, and only these # two actions count towards the requirement. The previously # defined total capacities remain in force. required <- capacities |> add_constraint_action_cardinality( count = 1, sense = "min", actions = c("restore", "control"), pu = c(10L, 20L), name = "management_required" ) # EXAMPLE 4: Require an exact count or prohibit interventions # An exact count of two overrides the default one-action maximum # in unit 10 and requires two distinct actions to be selected. exact <- add_constraint_action_cardinality( problem, count = 2, sense = "equal", pu = 10L ) # A zero maximum prohibits all actions in unit 30. # It does not exclude that unit from the planning region. no_action <- add_constraint_action_cardinality( problem, count = 0, sense = "max", pu = 30L ) # EXAMPLE 5: Map where a more flexible capacity is allowed # Use the bundled 64-unit landscape to impose a hypothetical # policy allowing two concurrent actions in the eastern half. # All other planning units retain the default maximum of one. # No solver is needed to visualise the scope of the rule. if (requireNamespace("sf", quietly = TRUE)) { sim <- load_sim_multiaction() spatial_problem <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # Identify the eastern units using the x-coordinate of their # polygon centroids. The two-action limit is an upper bound, # not a requirement to select two actions. centres <- sf::st_coordinates( sf::st_centroid(sf::st_geometry(sim$planning_units)) ) east <- centres[, 1] > stats::median(centres[, 1]) eastern_ids <- sim$planning_units$id[east] spatial_problem <- add_constraint_action_cardinality( spatial_problem, count = 2, sense = "max", pu = eastern_ids ) # Display the geographical limits, not selected actions. capacity_map <- sim$planning_units capacity_map$max_actions <- ifelse(east, 2L, 1L) plot(capacity_map["max_actions"], main = "Maximum actions per unit") }# EXAMPLE 1: Define the available decisions # Consider three planning units, each with four possible actions. # By default, the model allows at most one action in each unit. # Registering actions does not select any of them. problem <- create_problem( pu = data.frame(id = c(10L, 20L, 30L), cost = 1), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame( pu = c(10L, 20L, 30L), feature = 1L, amount = 10 ) ) |> add_actions( data.frame(id = c("restore", "control", "fence", "monitor")), cost = 1 ) # EXAMPLE 2: Different capacities in different planning units # Allow up to three actions in unit 10 and up to two in unit 20. # Unit 30 retains the default maximum of one action. # A maximum permits fewer actions, including no intervention. capacities <- problem |> add_constraint_action_cardinality( count = 3, sense = "max", pu = 10L, name = "capacity_10" ) |> add_constraint_action_cardinality( count = 2, sense = "max", pu = 20L, name = "capacity_20" ) # Inspect the registered constraints, not solved decisions. capacities$data$constraints$action_cardinality[ , c("name", "count", "sense") ] # EXAMPLE 3: Require at least one action from a subset # In units 10 and 20, require restoration or control (or both). # The minimum applies separately to each unit, and only these # two actions count towards the requirement. The previously # defined total capacities remain in force. required <- capacities |> add_constraint_action_cardinality( count = 1, sense = "min", actions = c("restore", "control"), pu = c(10L, 20L), name = "management_required" ) # EXAMPLE 4: Require an exact count or prohibit interventions # An exact count of two overrides the default one-action maximum # in unit 10 and requires two distinct actions to be selected. exact <- add_constraint_action_cardinality( problem, count = 2, sense = "equal", pu = 10L ) # A zero maximum prohibits all actions in unit 30. # It does not exclude that unit from the planning region. no_action <- add_constraint_action_cardinality( problem, count = 0, sense = "max", pu = 30L ) # EXAMPLE 5: Map where a more flexible capacity is allowed # Use the bundled 64-unit landscape to impose a hypothetical # policy allowing two concurrent actions in the eastern half. # All other planning units retain the default maximum of one. # No solver is needed to visualise the scope of the rule. if (requireNamespace("sf", quietly = TRUE)) { sim <- load_sim_multiaction() spatial_problem <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # Identify the eastern units using the x-coordinate of their # polygon centroids. The two-action limit is an upper bound, # not a requirement to select two actions. centres <- sf::st_coordinates( sf::st_centroid(sf::st_geometry(sim$planning_units)) ) east <- centres[, 1] > stats::median(centres[, 1]) eastern_ids <- sim$planning_units$id[east] spatial_problem <- add_constraint_action_cardinality( spatial_problem, count = 2, sense = "max", pu = eastern_ids ) # Display the geographical limits, not selected actions. capacity_map <- sim$planning_units capacity_map$max_actions <- ifelse(east, 2L, 1L) plot(capacity_map["max_actions"], main = "Maximum actions per unit") }
Prevent two or more incompatible actions from being selected together in the same planning unit. At most one action in the specified group may be selected; selecting none is also allowed.
add_constraint_action_excludes(x, actions, pu = NULL, name = NULL)add_constraint_action_excludes(x, actions, pu = NULL, name = NULL)
x |
A |
actions |
At least two action IDs or existing classification labels that must be mutually exclusive in each scoped planning unit. |
pu |
Optional external planning-unit IDs. |
name |
Optional unique, non-empty constraint label. If omitted, a label is generated automatically. |
How the relation works
Suppose restoration and harvesting are incompatible uses of a site. This relation prevents their simultaneous selection without prohibiting either action individually. With three or more actions, every pair in the group is mutually exclusive, not just the complete combination.
For every scoped unit, the constraint is
. Actions outside the group are not
restricted by this relation. The rule is most informative when the
cardinality settings otherwise permit more than one action per unit.
A new Problem with the relation appended. The input problem is
unchanged.
Each relation applies separately within each unit specified by pu;
pu = NULL applies it to all registered planning units. Relations do not
select actions by themselves or make unavailable actions feasible.
By default, at most one action can be selected per planning unit.
Use add_constraint_action_cardinality() when a requires or together rule
needs simultaneous selections. Unavailable or locked-out actions are treated
as unselected: missing companions may prohibit a trigger, and a missing
together member may prohibit the other members. Combined rules, budgets,
and locks can also make a model infeasible.
Relations do not imply a temporal sequence or dependencies between different
planning units. They can be combined with joint effects registered through
add_action_sets(), but defining a joint effect does not itself require
actions to be selected together.
Distinct rules accumulate and are enforced simultaneously. Duplicate rules with the same type, action groups, sense, and PU scope are rejected; names must be unique across requires, excludes, and together relations. To revise a rule, start from the problem before that rule was added.
add_constraint_action_requires(), add_constraint_action_together(),
add_constraint_action_cardinality()
# EXAMPLE 1: Allow multiple actions, but exclude an incompatible pair # Up to three actions may be selected per unit. Restoration and harvesting # are incompatible, while threat control can accompany either one. base <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "harvest", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") exclusive <- add_constraint_action_excludes( base, actions = c("restore", "harvest"), name = "incompatible_uses" ) # EXAMPLE 2: Restrict the rule to selected planning units # In unit 1, choosing any one of restoration, harvesting, or control # precludes the other two. The same rule is not imposed in unit 2. local <- add_constraint_action_excludes( base, actions = c("restore", "harvest", "control"), pu = 1L, name = "local_exclusion" ) # Inspect the stored relation, not a selected management plan. local$data$constraints$action_relations[, c("type", "name")]# EXAMPLE 1: Allow multiple actions, but exclude an incompatible pair # Up to three actions may be selected per unit. Restoration and harvesting # are incompatible, while threat control can accompany either one. base <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "harvest", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") exclusive <- add_constraint_action_excludes( base, actions = c("restore", "harvest"), name = "incompatible_uses" ) # EXAMPLE 2: Restrict the rule to selected planning units # In unit 1, choosing any one of restoration, harvesting, or control # precludes the other two. The same rule is not imposed in unit 2. local <- add_constraint_action_excludes( base, actions = c("restore", "harvest", "control"), pu = 1L, name = "local_exclusion" ) # Inspect the stored relation, not a selected management plan. local$data$constraints$action_relations[, c("type", "name")]
Make the selection of one or more actions conditional on selecting
companion actions in the same planning unit. With sense = "all", every
companion is required; with sense = "any", at least one is required.
add_constraint_action_requires( x, actions, requires, sense = c("all", "any"), pu = NULL, name = NULL )add_constraint_action_requires( x, actions, requires, sense = c("all", "any"), pu = NULL, name = NULL )
x |
A |
actions |
Non-empty vector of triggering action IDs or existing
|
requires |
Non-empty vector of companion action IDs or existing
classification labels, disjoint from |
sense |
|
pu |
Optional external planning-unit IDs. |
name |
Optional unique, non-empty constraint label. If omitted, a label is generated automatically. |
How the relation works
Suppose restoration is effective only when threat control is also carried out. If restoration is selected, control must also be selected; if restoration is not selected, the relation imposes no requirement. With several triggering actions, each independently activates the requirement.
For binary action decisions , requiring all companions gives
for each required action . Requiring any
companion gives . When there is only one
companion, "all" and "any" are equivalent.
A new Problem with the relation appended to
x$data$constraints$action_relations. The input problem is unchanged.
Each relation applies separately within each unit specified by pu;
pu = NULL applies it to all registered planning units. Relations do not
select actions by themselves or make unavailable actions feasible.
By default, at most one action can be selected per planning unit.
Use add_constraint_action_cardinality() when a requires or together rule
needs simultaneous selections. Unavailable or locked-out actions are treated
as unselected: missing companions may prohibit a trigger, and a missing
together member may prohibit the other members. Combined rules, budgets,
and locks can also make a model infeasible.
Relations do not imply a temporal sequence or dependencies between different
planning units. They can be combined with joint effects registered through
add_action_sets(), but defining a joint effect does not itself require
actions to be selected together.
Distinct rules accumulate and are enforced simultaneously. Duplicate rules with the same type, action groups, sense, and PU scope are rejected; names must be unique across requires, excludes, and together relations. To revise a rule, start from the problem before that rule was added.
add_constraint_action_excludes(), add_constraint_action_together(),
add_constraint_action_cardinality(), add_action_sets()
# EXAMPLE 1: Create a problem where concurrent actions are possible # Three planning units offer restoration, control, fencing, and monitoring. # Allow up to three actions per unit: the default one-action maximum # would otherwise prevent restoration and its companions being selected. base <- create_problem( pu = data.frame(id = c(10L, 20L, 30L), cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame( pu = c(10L, 20L, 30L), feature = 1L, amount = 100 ) ) |> add_actions( data.frame(id = c("restore", "control", "fence", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") # EXAMPLE 2: Require all companions in a specific unit # In unit 10, restoration must be accompanied by both control and fencing. # This is a conditional requirement: it does not force restoration. required <- add_constraint_action_requires( base, actions = "restore", requires = c("control", "fence"), sense = "all", pu = 10L, name = "restore_with_both" ) # EXAMPLE 3: Require at least one alternative companion # In unit 20, either control OR fencing is sufficient for restoration. # These rules have different PU scopes and can coexist. required <- add_constraint_action_requires( required, actions = "restore", requires = c("control", "fence"), sense = "any", pu = 20L, name = "restore_with_either" ) # EXAMPLE 4: Multiple independent triggers # In unit 30, choosing either restoration or monitoring requires control. # The triggers need not both be selected. required <- add_constraint_action_requires( required, actions = c("restore", "monitor"), requires = "control", pu = 30L ) # Inspect the three registered rules (not an optimised solution). required$data$constraints$action_relations[, c("type", "sense", "name")] # EXAMPLE 5: Map where a requirement applies (no solver needed) if (requireNamespace("sf", quietly = TRUE)) { sim <- load_sim_multiaction() # Apply the restoration-protection requirement to the first 16 cells. # Both actions may be selected in these cells, so permit cardinality 2. scoped_pu <- head(sim$planning_units$id, 16) spatial <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_constraint_action_cardinality(2, "max", pu = scoped_pu) |> add_constraint_action_requires( "restore", "protect", pu = scoped_pu ) # The map shows the SCOPE of the requirement, not selected actions. mapped <- sim$planning_units mapped$requirement <- ifelse( mapped$id %in% scoped_pu, "Applies", "Does not apply" ) plot(mapped["requirement"], main = "Restoration requires protection") }# EXAMPLE 1: Create a problem where concurrent actions are possible # Three planning units offer restoration, control, fencing, and monitoring. # Allow up to three actions per unit: the default one-action maximum # would otherwise prevent restoration and its companions being selected. base <- create_problem( pu = data.frame(id = c(10L, 20L, 30L), cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame( pu = c(10L, 20L, 30L), feature = 1L, amount = 100 ) ) |> add_actions( data.frame(id = c("restore", "control", "fence", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") # EXAMPLE 2: Require all companions in a specific unit # In unit 10, restoration must be accompanied by both control and fencing. # This is a conditional requirement: it does not force restoration. required <- add_constraint_action_requires( base, actions = "restore", requires = c("control", "fence"), sense = "all", pu = 10L, name = "restore_with_both" ) # EXAMPLE 3: Require at least one alternative companion # In unit 20, either control OR fencing is sufficient for restoration. # These rules have different PU scopes and can coexist. required <- add_constraint_action_requires( required, actions = "restore", requires = c("control", "fence"), sense = "any", pu = 20L, name = "restore_with_either" ) # EXAMPLE 4: Multiple independent triggers # In unit 30, choosing either restoration or monitoring requires control. # The triggers need not both be selected. required <- add_constraint_action_requires( required, actions = c("restore", "monitor"), requires = "control", pu = 30L ) # Inspect the three registered rules (not an optimised solution). required$data$constraints$action_relations[, c("type", "sense", "name")] # EXAMPLE 5: Map where a requirement applies (no solver needed) if (requireNamespace("sf", quietly = TRUE)) { sim <- load_sim_multiaction() # Apply the restoration-protection requirement to the first 16 cells. # Both actions may be selected in these cells, so permit cardinality 2. scoped_pu <- head(sim$planning_units$id, 16) spatial <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_constraint_action_cardinality(2, "max", pu = scoped_pu) |> add_constraint_action_requires( "restore", "protect", pu = scoped_pu ) # The map shows the SCOPE of the requirement, not selected actions. mapped <- sim$planning_units mapped$requirement <- ifelse( mapped$id %in% scoped_pu, "Applies", "Does not apply" ) plot(mapped["requirement"], main = "Restoration requires protection") }
Link a group of actions so that, in each specified planning unit, either all are selected or none are selected.
add_constraint_action_together(x, actions, pu = NULL, name = NULL)add_constraint_action_together(x, actions, pu = NULL, name = NULL)
x |
A |
actions |
At least two action IDs or existing classification labels that must be selected together or omitted together in each scoped unit. |
pu |
Optional external planning-unit IDs. |
name |
Optional unique, non-empty constraint label. If omitted, a label is generated automatically. |
How the relation works
Suppose restoration and threat control must be implemented as one package. This relation requires both actions whenever either is selected. It does not require either action to be selected in the first place.
The binary decisions satisfy for all members of the
group. Unlike add_constraint_action_requires(), dependency is
bidirectional. Unlike add_constraint_action_excludes(), simultaneous
selection is permitted and, when the group is used, required.
Cardinality must allow all members to be selected. Registering a set with
add_action_sets() specifies a possible combination for joint effects;
it does not itself impose the together relation.
A new Problem with the relation appended. The input problem is
unchanged.
Each relation applies separately within each unit specified by pu;
pu = NULL applies it to all registered planning units. Relations do not
select actions by themselves or make unavailable actions feasible.
By default, at most one action can be selected per planning unit.
Use add_constraint_action_cardinality() when a requires or together rule
needs simultaneous selections. Unavailable or locked-out actions are treated
as unselected: missing companions may prohibit a trigger, and a missing
together member may prohibit the other members. Combined rules, budgets,
and locks can also make a model infeasible.
Relations do not imply a temporal sequence or dependencies between different
planning units. They can be combined with joint effects registered through
add_action_sets(), but defining a joint effect does not itself require
actions to be selected together.
Distinct rules accumulate and are enforced simultaneously. Duplicate rules with the same type, action groups, sense, and PU scope are rejected; names must be unique across requires, excludes, and together relations. To revise a rule, start from the problem before that rule was added.
add_constraint_action_requires(), add_constraint_action_excludes(),
add_constraint_action_cardinality(), add_action_sets()
# EXAMPLE 1: Require restoration and control to occur together # In planning unit 1, restoration and control must be selected together # or both omitted. Monitoring remains independently selectable. # Allow two simultaneous actions in unit 1; elsewhere the default # maximum of one action still applies. base <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(2, "max", pu = 1L) together <- add_constraint_action_together( base, actions = c("restore", "control"), pu = 1L, name = "restoration_package" ) # Inspect the registered requirement; no solver has been run. together$data$constraints$action_relations[, c("type", "name")] # EXAMPLE 2: Extend the package to three actions # All three must be selected or omitted as a group. The cardinality # upper bound must now allow three actions in the scoped unit. extended <- create_problem( pu = data.frame(id = 1L, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1L, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") |> add_constraint_action_together(c("restore", "control", "monitor"))# EXAMPLE 1: Require restoration and control to occur together # In planning unit 1, restoration and control must be selected together # or both omitted. Monitoring remains independently selectable. # Allow two simultaneous actions in unit 1; elsewhere the default # maximum of one action still applies. base <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(2, "max", pu = 1L) together <- add_constraint_action_together( base, actions = c("restore", "control"), pu = 1L, name = "restoration_package" ) # Inspect the registered requirement; no solver has been run. together$data$constraints$action_relations[, c("type", "name")] # EXAMPLE 2: Extend the package to three actions # All three must be selected or omitted as a group. The cardinality # upper bound must now allow three actions in the scoped unit. extended <- create_problem( pu = data.frame(id = 1L, cost = 0), features = data.frame(id = 1L, name = "habitat"), dist_features = data.frame(pu = 1L, feature = 1L, amount = 100) ) |> add_actions( data.frame(id = c("restore", "control", "monitor")), cost = 1 ) |> add_constraint_action_cardinality(3, "max") |> add_constraint_action_together(c("restore", "control", "monitor"))
Add an area constraint to a planning problem.
This function stores one area-constraint specification in the
Problem object so that it can later be incorporated when the
optimization model is assembled. Area constraints can be imposed either on
the total selected planning-unit area or on the effective area of selected
actions.
Multiple area constraints can be added by calling this function repeatedly, provided that no duplicated combination of action subset and constraint sense is introduced.
add_constraint_area( x, area, sense, tolerance = 0, area_col = NULL, area_unit = c("m2", "ha", "km2"), actions = NULL, name = NULL )add_constraint_area( x, area, sense, tolerance = 0, area_col = NULL, area_unit = c("m2", "ha", "km2"), actions = NULL, name = NULL )
x |
A |
area |
Numeric scalar greater than or equal to zero. Target value for the constrained area. |
sense |
Character string indicating the type of area constraint. Must be
one of |
tolerance |
Numeric scalar greater than or equal to zero. Only used when
|
area_col |
Optional character string giving the name of the planning-unit
area column. When |
area_unit |
Character string indicating the unit of |
actions |
Optional subset of actions to which the constraint applies.
If |
name |
Optional character string used as the label of the stored linear
constraint when it is later added to the optimization model. If
|
Calls accumulate constraints for distinct action scopes, area measures
(area_col), or senses. Repeating the same combination raises an error,
even with a different threshold, display unit, or name. To change it, rebuild
from the problem before the constraint was added.
Explicit names must be unique within the area-constraint family; omitted
names are generated automatically.
Use this function when area requirements must be imposed either on the total selected landscape or on the effective area allocated to specific actions.
Total selected area.
Let denote the set of planning units and let
be the area associated with planning unit
.
When actions = NULL, the constraint refers to the total selected area
in the problem. In that case, let denote the binary
variable indicating whether planning unit is selected by at least one
decision in the model.
Depending on sense, this function stores one of the following
constraints.
If sense = "min":
If sense = "max":
If sense = "equal" and tolerance = 0:
If sense = "equal" and tolerance > 0, the equality is stored as
a two-sided band:
where is the value supplied through tolerance.
Action-specific effective area.
When actions is not NULL, the constraint refers to the
effective area of the selected decisions associated with the specified subset
of actions.
Let denote the specified
action subset and let denote the binary variable
indicating whether action is selected in
planning unit . Let denote the
effective action area associated with feasible pair , as stored in
x$data$dist_actions$action_area. In that case, the constrained
quantity is:
Effective action areas are defined in add_actions. They can be
supplied manually through the action_area argument, derived from
spatial include_pairs, or default to full planning-unit areas when
these can be derived from the problem.
Consequently, if no partial action areas are supplied and each feasible action is assumed to apply to the full planning unit, then an action-specific area constraint is numerically equivalent to using planning-unit areas for those action decisions. If partial action areas are supplied, the constraint uses those effective areas instead.
Area units and area sources.
The value of area_unit indicates the unit in which area and
tolerance are expressed.
When actions = NULL, areas are obtained from the planning-unit table or
planning-unit geometry. If area_col is provided, that column is used.
Otherwise, the model builder determines the default area source according to
the internal rules of the package.
When actions is not NULL, the model builder uses
x$data$dist_actions$action_area. These values are stored internally in
square metres and are converted to area_unit when the model is built.
If action areas are missing for relevant feasible (pu, action) pairs,
the model builder may use area_col as a fallback to derive full
planning-unit areas. If valid action areas still cannot be obtained, model
construction fails with an error.
This function only stores the constraint specification; it does not validate the feasibility of the threshold against the available planning units or actions at this stage.
Multiple area constraints can be stored in a Problem object. However,
at most one can be stored for the same combination of action subset and
constraint sense. Attempting to add a duplicated
actions–sense combination results in an error.
An updated Problem object with the new area-constraint
specification appended to x$data$constraints$area.
# 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 ) # Constrain the total selected planning-unit area. p <- add_constraint_area( x = p, area = 25, sense = "min", area_unit = "ha" ) # Constrain the effective area allocated to the restore action. # Action areas are derived from the planning-unit geometries. p <- add_constraint_area( x = p, area = 15, sense = "max", area_unit = "ha", actions = "restore" ) p$data$constraints$area# 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 ) # Constrain the total selected planning-unit area. p <- add_constraint_area( x = p, area = 25, sense = "min", area_unit = "ha" ) # Constrain the effective area allocated to the restore action. # Action areas are derived from the planning-unit geometries. p <- add_constraint_area( x = p, area = 15, sense = "max", area_unit = "ha", actions = "restore" ) p$data$constraints$area
Add a budget constraint to a planning problem.
This function stores one budget-constraint specification in the
Problem object so that it can later be incorporated when the
optimization model is assembled. Multiple budget constraints can be added by
calling this function repeatedly, provided that no duplicated combination of
action subset and constraint sense is introduced.
add_constraint_budget( x, budget, sense, tolerance = 0, actions = NULL, include_pu_cost = TRUE, include_action_cost = TRUE, name = NULL )add_constraint_budget( x, budget, sense, tolerance = 0, actions = NULL, include_pu_cost = TRUE, include_action_cost = TRUE, name = NULL )
x |
A |
budget |
Numeric scalar greater than or equal to zero. Target value for the constrained budget. |
sense |
Character string indicating the type of budget constraint. Must
be one of |
tolerance |
Numeric scalar greater than or equal to zero. Only used when
|
actions |
Optional subset of actions to which the constraint applies.
If |
include_pu_cost |
Logical scalar indicating whether planning-unit costs
should be included in the constrained budget. This is only supported when
|
include_action_cost |
Logical scalar indicating whether action costs should be included in the constrained budget. |
name |
Optional character string used as the label of the stored linear
constraint when it is later added to the optimization model. If
|
Calls accumulate constraints for distinct action scopes, cost components, or senses. Repeating the same combination raises an error, even with a different threshold or name. To change it, rebuild from the problem before the constraint was added. Explicit names must be unique within the budget-constraint family; omitted names are generated automatically.
Use this function when spending limits or minimum spending requirements must be imposed either on the full problem or on the subset of selected decisions associated with specific actions.
Let denote the set of planning units and let
denote the set of actions. Let
denote the binary variable indicating whether planning
unit is selected by at least one decision in the
model, and let denote the binary variable
indicating whether action is selected in planning
unit .
The total constrained budget can include two cost components:
planning-unit costs, associated with ;
action costs, associated with .
The arguments include_pu_cost and include_action_cost
determine which of these components are included in the stored budget
constraint.
When actions = NULL, the constraint refers to the total budget across
the whole problem. In that case, depending on the values of
include_pu_cost and include_action_cost, the constrained
quantity is one of the following:
If only planning-unit costs are included:
If only action costs are included:
If both components are included:
Depending on sense, this function stores one of the following
constraints:
If sense = "min":
If sense = "max":
If sense = "equal" and tolerance = 0:
If sense = "equal" and tolerance > 0, the equality is stored as
a two-sided band:
where denotes the selected cost expression and is the
value supplied through tolerance.
When actions is not NULL, only action costs can be included.
In that case, let denote the
selected subset of actions, and the constrained quantity is
Action-specific budget constraints only support action costs. Therefore,
include_pu_cost = TRUE is only allowed when actions = NULL,
because planning-unit costs are not action-specific.
This function only stores the constraint specification; it does not validate the feasibility of the threshold against the available cost data at this stage.
Multiple budget constraints can be stored in a Problem object.
However, at most one can be stored for the same combination of action subset
and constraint sense. Attempting to add a duplicated
actions–sense combination results in an error.
An updated Problem object with the new budget-constraint
specification appended to the stored budget-constraint table.
# 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 ) p <- add_constraint_budget( x = p, budget = 10, sense = "max", include_pu_cost = TRUE, include_action_cost = TRUE ) p <- add_constraint_budget( x = p, budget = 4, sense = "max", actions = "restore", include_pu_cost = FALSE, include_action_cost = TRUE ) p <- add_constraint_budget( x = p, budget = 1, sense = "min", actions = "restore", include_pu_cost = FALSE, include_action_cost = TRUE ) p$data$constraints$budget# 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 ) p <- add_constraint_budget( x = p, budget = 10, sense = "max", include_pu_cost = TRUE, include_action_cost = TRUE ) p <- add_constraint_budget( x = p, budget = 4, sense = "max", actions = "restore", include_pu_cost = FALSE, include_action_cost = TRUE ) p <- add_constraint_budget( x = p, budget = 1, sense = "min", actions = "restore", include_pu_cost = FALSE, include_action_cost = TRUE ) p$data$constraints$budget
Fix feasible planning unit–action decisions to be selected or excluded.
This function modifies the status of existing feasible
(pu, action) pairs stored in the feasible action table. It does not
create new feasible action pairs and therefore must be used only after
add_actions has been called.
add_constraint_locked_actions(x, locked_in = NULL, locked_out = NULL)add_constraint_locked_actions(x, locked_in = NULL, locked_out = NULL)
x |
A |
locked_in |
Optional specification of feasible |
locked_out |
Optional specification of feasible |
Use this function when only specific feasible (pu, action) decisions
must be forced in or out of the solution, rather than whole planning units.
Let denote the set of planning units and
the set of actions. Let
denote the set of
feasible planning unit–action pairs already defined in the problem.
This function allows the user to define two subsets:
, the set of feasible
pairs that must be selected,
, the set of feasible
pairs that must not be selected.
These sets are encoded by updating the status column of the feasible
action table. The function validates that all requested locked-in and
locked-out pairs are already feasible. Therefore, it cannot be used to
introduce new planning unit–action combinations into the problem.
In optimization terms, if denotes the decision variable
associated with planning unit and action , then:
locked-in pairs conceptually impose ,
locked-out pairs conceptually impose .
The exact translation into solver-side constraints occurs later when the model is built.
In contrast, add_constraint_locked_pu fixes whole planning
units through the unit-selection variables, whereas this function fixes only
specific feasible (pu, action) decisions.
Accepted formats
Both locked_in and locked_out accept the same formats:
NULL,
a data.frame with columns pu and action,
optionally including a feasible column used as a filter,
a named list whose names are action ids and whose elements are either
vectors of planning unit ids or sf objects.
If a feasible column is supplied in a data.frame, only rows
with feasible = TRUE are used. Missing values in feasible are
treated as FALSE.
If an sf specification is supplied, the problem object must contain
planning-unit geometry, and planning units are matched spatially using
sf::st_intersects().
Conflict checking
Calls accumulate compatible locks and preserve omitted arguments. Repeating the same lock is idempotent. Changing an existing locked-in pair to locked-out, or vice versa, raises an error. To change a lock, rebuild from the problem before it was added.
A given (pu, action) pair cannot be simultaneously requested in both
locked_in and locked_out. Such overlaps are rejected.
In addition, if a planning unit is already marked as locked out at the
planning-unit level, then all feasible actions in that planning unit are
forced to status = 3. Any attempt to lock in an action within such a
planning unit raises an error.
Order of precedence
User-supplied locked-in and locked-out action requests are first applied to
the feasible action table. Afterwards, any planning-unit-level
locked_out flag is enforced, overriding action-level status and
ensuring consistency with planning-unit exclusions.
An updated Problem object in which the status column of the
feasible action table has been modified to reflect locked-in and locked-out
decisions.
add_actions,
add_constraint_locked_pu
# Build a small spatial planning problem with two available actions. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # EXAMPLE 1: Fix individual planning-unit/action decisions. # Protection must be selected in unit 1, restoration in unit 2, # and protection cannot be selected in unit 4. locked <- add_constraint_locked_actions( p, locked_in = data.frame( pu = c(1, 2), action = c("protect", "restore") ), locked_out = data.frame(pu = 4, action = "protect") ) # Only show modified decisions: 2 = locked in, 3 = locked out. # All remaining feasible decisions retain status 0 (free). subset(locked$data$dist_actions, status %in% c(2L, 3L), select = c(pu, action, status)) # EXAMPLE 2: Specify the same locks using a named list. # List names are action IDs; values are planning-unit IDs. locked_list <- add_constraint_locked_actions( p, locked_in = list(protect = 1, restore = 2), locked_out = list(protect = 4) ) # Both input formats lead to the same decision statuses. identical(locked$data$dist_actions$status, locked_list$data$dist_actions$status)# Build a small spatial planning problem with two available actions. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # EXAMPLE 1: Fix individual planning-unit/action decisions. # Protection must be selected in unit 1, restoration in unit 2, # and protection cannot be selected in unit 4. locked <- add_constraint_locked_actions( p, locked_in = data.frame( pu = c(1, 2), action = c("protect", "restore") ), locked_out = data.frame(pu = 4, action = "protect") ) # Only show modified decisions: 2 = locked in, 3 = locked out. # All remaining feasible decisions retain status 0 (free). subset(locked$data$dist_actions, status %in% c(2L, 3L), select = c(pu, action, status)) # EXAMPLE 2: Specify the same locks using a named list. # List names are action IDs; values are planning-unit IDs. locked_list <- add_constraint_locked_actions( p, locked_in = list(protect = 1, restore = 2), locked_out = list(protect = 4) ) # Both input formats lead to the same decision statuses. identical(locked$data$dist_actions$status, locked_list$data$dist_actions$status)
Define planning units that must be included in, or excluded from, the optimization problem.
This function updates the planning-unit table stored in the Problem
object by creating or replacing the logical columns locked_in and
locked_out. These columns are later used by the model builder when
translating the problem into optimization constraints.
Lock information may be supplied either directly as logical vectors, as
vectors of planning-unit ids, or by referencing columns in the raw
planning-unit data originally passed to create_problem.
add_constraint_locked_planning_units(x, locked_in = NULL, locked_out = NULL)add_constraint_locked_planning_units(x, locked_in = NULL, locked_out = NULL)
x |
A |
locked_in |
Optional locked-in specification. It may be |
locked_out |
Optional locked-out specification. It may be |
Use this function when whole planning units must be forced into or out of the solution, regardless of which action may later be selected in them.
Let denote the set of planning units and let
denote the binary variable indicating whether planning
unit is selected by the model.
This function defines two subsets:
, the planning units that
must be included,
, the planning units
that must be excluded.
Conceptually, these sets correspond to the following conditions:
if , then ,
if , then .
These constraints are not imposed immediately by this function; instead, they are stored in the planning-unit table and enforced later when building the optimization model.
Philosophy
The role of create_problem is to construct and normalize the basic
inputs of the planning problem. Locking planning units is treated as a
separate modelling step so that users can define or revise selection
restrictions after the Problem object has already been created.
In contrast, add_constraint_locked_actions is used to fix
specific feasible (pu, action) decisions rather than whole planning
units.
Supported input formats
For both locked_in and locked_out, the function accepts:
NULL, meaning that no planning units are locked on that side,
a single character string, interpreted as a column name in the raw planning-unit data,
a logical vector of length nrow(pu),
a vector of planning-unit ids.
When a column name is supplied, the referenced column is coerced to logical.
Numeric values are interpreted as non-zero = TRUE; character and
factor values are interpreted using common logical strings such as
"true", "t", "1", "yes", and "y".
Missing values are treated as FALSE.
Repeated calls
Calls accumulate compatible locks and preserve omitted arguments. Repeating the same lock is idempotent. A planning unit cannot be locked in after it was locked out, or vice versa. Conflicts with existing action locks are also rejected. To change a lock, rebuild from the problem before it was added.
Consistency checks
The function checks that no planning unit is simultaneously assigned to both
locked_in and locked_out. If such conflicts are found, an error
is raised.
An updated Problem object in which the planning-unit table
contains logical columns locked_in and locked_out.
create_problem,
add_actions,
add_constraint_locked_actions,
add_constraint_locked_pu
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() pu <- example_data$planning_units pu$lock_col <- pu$id %in% c(1, 4) pu$out_col <- pu$id == 5 p <- create_problem( pu = pu, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) # 1) Lock by planning-unit ids p1 <- add_constraint_locked_planning_units( x = p, locked_in = c(1, 3), locked_out = c(5) ) p1$data$pu[, c("id", "locked_in", "locked_out")] # 2) Read lock information from raw planning-unit data columns p2 <- add_constraint_locked_planning_units( x = p, locked_in = "lock_col", locked_out = "out_col" ) p2$data$pu[, c("id", "locked_in", "locked_out")] # 3) Use logical vectors p3 <- add_constraint_locked_planning_units( x = p, locked_in = pu$id %in% c(1, 3), locked_out = pu$id == 4 ) p3$data$pu[, c("id", "locked_in", "locked_out")]# Load a complete simulated planning problem. example_data <- load_sim_multiaction() pu <- example_data$planning_units pu$lock_col <- pu$id %in% c(1, 4) pu$out_col <- pu$id == 5 p <- create_problem( pu = pu, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) # 1) Lock by planning-unit ids p1 <- add_constraint_locked_planning_units( x = p, locked_in = c(1, 3), locked_out = c(5) ) p1$data$pu[, c("id", "locked_in", "locked_out")] # 2) Read lock information from raw planning-unit data columns p2 <- add_constraint_locked_planning_units( x = p, locked_in = "lock_col", locked_out = "out_col" ) p2$data$pu[, c("id", "locked_in", "locked_out")] # 3) Use logical vectors p3 <- add_constraint_locked_planning_units( x = p, locked_in = pu$id %in% c(1, 3), locked_out = pu$id == 4 ) p3$data$pu[, c("id", "locked_in", "locked_out")]
add_constraint_locked_pu() has been replaced by
add_constraint_locked_planning_units.
add_constraint_locked_pu(x, locked_in = NULL, locked_out = NULL)add_constraint_locked_pu(x, locked_in = NULL, locked_out = NULL)
x |
A |
locked_in |
Optional locked-in specification. It may be |
locked_out |
Optional locked-out specification. It may be |
An updated Problem object in which the planning-unit table
contains logical columns locked_in and locked_out.
add_constraint_locked_planning_units
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.
add_constraint_targets_absolute( x, targets, features = NULL, actions = NULL, label = NULL )add_constraint_targets_absolute( x, targets, features = NULL, actions = NULL, label = NULL )
x |
A |
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 |
features |
Optional feature specification indicating which features the
supplied target values refer to when |
actions |
Optional character vector indicating which actions count toward
target achievement. Entries may match action ids, |
label |
Optional character string stored with the targets for reporting and bookkeeping. |
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 denote the set of features. For each targeted feature
, this function stores an absolute target threshold
.
With at most one action per unit, each target is interpreted as a lower-bound constraint on post-action amounts of the form:
where:
indexes planning units,
indexes actions,
indicates whether action is selected in planning
unit ,
is the contribution of that action to feature
,
is the subset of planning unit–action
pairs allowed to count toward the target for feature .
In the absolute case, the stored target threshold is simply:
where is the user-supplied target value for feature .
With concurrent actions, the model instead combines signed changes within each unit and feature and counts the reference once:
Here 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 actions argument restricts which actions may contribute toward
achievement of the target, but it does not modify the value of
itself.
The targets argument is parsed by .pa_parse_targets() and may be
supplied in several equivalent forms, including:
a single numeric value recycled to all selected features,
a numeric vector aligned to features,
a named numeric vector where names identify features,
a data.frame with feature and target columns.
If targets does not explicitly identify features:
if features = NULL, the target is applied to all features,
if 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.
An updated Problem object with absolute targets appended to
the stored target table.
add_constraint_targets_relative
# 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# 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
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 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.
add_constraint_targets_relative( x, targets, features = NULL, actions = NULL, label = NULL )add_constraint_targets_relative( x, targets, features = NULL, actions = NULL, label = NULL )
x |
A |
targets |
Target specification as proportions in |
features |
Optional feature specification indicating which features the
supplied target values refer to when |
actions |
Optional character vector indicating which actions count toward
target achievement. Entries may match action ids, |
label |
Optional character string stored with the targets for reporting and bookkeeping. |
Use this function when target requirements are naturally expressed as proportions of reference-scenario feature totals rather than in original feature units.
Let denote the set of features. For each targeted feature
, let denote the reference-scenario total
amount of that feature in the landscape, as computed by
.pa_feature_totals().
If the user supplies a relative target , then this
function converts it to an absolute threshold:
The absolute threshold is stored in target_value, while:
the original user-supplied proportion is stored in
target_raw,
the baseline total is stored in basis_total.
When the optimization model is built, the resulting target is interpreted as:
where:
indexes planning units,
indexes actions,
indicates whether action is selected in planning
unit ,
is the contribution of that action to feature
,
is the subset of planning unit–action
pairs allowed to count toward the target for feature .
With concurrent actions, the model instead combines signed changes within each unit and feature and counts the reference once:
Here 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 actions argument restricts which actions may contribute toward
target achievement, but it does not affect the baseline amount used
to compute the threshold. In other words, relative targets are always scaled
against the full landscape reference scenario.
Therefore, actions changes who may satisfy the target, but not how the
threshold itself is scaled.
The targets argument is parsed by .pa_parse_targets() and may be
supplied in several equivalent forms, including:
a single numeric value recycled to all selected features,
a numeric vector aligned to features,
a named numeric vector where names identify features,
a data.frame with feature and target columns.
If targets does not explicitly identify features:
if features = NULL, the target is applied to all features,
if features is supplied, the target values are interpreted with
respect to that feature set.
Relative targets must lie in .
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.
An updated Problem object with relative targets appended to
the stored target table.
add_constraint_targets_absolute
# 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# 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
Specify the expected consequences of feasible management actions for feature amounts in planning units, relative to a reference scenario.
add_effects( x, effects = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean"), component = c("any", "benefit", "loss"), raster_aggregation = c("sum", "mean"), raster_type = c("effect", "outcome") )add_effects( x, effects = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean"), component = c("any", "benefit", "loss"), raster_aggregation = c("sum", "mean"), raster_type = c("effect", "outcome") )
x |
A |
effects |
A data frame with |
effect_type |
Deprecated interpretation argument: |
effect_aggregation |
Deprecated raster aggregation argument; use
|
component |
Deprecated sign filter: |
raster_aggregation |
Aggregate raster values using |
raster_type |
Interpret raster values as signed changes
( |
Reference amounts, outcomes, and effects
The feature distribution supplied to create_problem() defines the
reference amount of feature in planning unit .
This reference may describe current conditions, existing management, or a
future without intervention. Let be the expected amount under
action . Its signed effect is .
These quantities describe what an action changes, not whether that change
is desirable. For example, increasing habitat may be desirable, whereas
decreasing fuel load may also be desirable. Objectives and constraints
determine how consequences are valued in a planning problem.
Three alternative tabular inputs
Provide action, feature, optionally pu, and exactly
one of the following numeric columns:
effect: signed absolute change .
outcome: expected amount under the action.
relative_change: proportional change , with
and
.
For a reference amount of 100, effect = 30,
outcome = 130, and relative_change = 0.30 are equivalent.
Likewise, relative_change = -0.20 represents a 20 percent decrease.
These input columns are alternatives, not values to supply together.
Actions must first be registered with add_actions(). With pu,
effects can vary by location; without it, each action/feature specification
is expanded over feasible planning-unit/action pairs. Locked-out pairs are
excluded. A feature can be identified by name or numeric identifier.
Values must be finite and non-missing; resulting outcomes must be
non-negative. Reference amounts and outcomes must have compatible units
and, when applicable, a common time horizon. Missing reference amounts are
treated as zero: a relative change cannot create a positive outcome from a
zero reference, so use effect or outcome in that case.
Stored representation
Regardless of the input format, dist_effects exposes
reference_amount, action_outcome, and signed effect.
For compatibility, it also retains amount_after (an alias of
action_outcome) and columns named benefit and loss:
these are strictly the positive and negative magnitudes of the signed
change, and , not ecological judgements.
A zero effect leaves the outcome equal to the reference amount.
Joint action effects
An action identifier may also refer to a set registered with
add_action_sets(). Supply its total joint effect or outcome,
not merely the interaction increment; individual and joint effects must be
supplied in the same call. In the absence of a specified interaction,
concurrent individual effects are treated as additive. Unspecified terms
are assumed to be zero, which is a modelling assumption. Joint effects and
signed interaction corrections are retained in separate internal tables;
dist_effects represents individual actions. Cardinality constraints
determine whether combinations may be selected.
Raster inputs
Supply a named list of terra::SpatRaster objects, one per action,
each containing one layer per feature in feature-catalogue order. The
problem must contain planning-unit polygons or a planning-unit raster.
Use raster_type = "effect" for signed changes or
raster_type = "outcome" for expected amounts, and choose
raster_aggregation = "sum" or "mean" to aggregate values
within planning units. Ensure the aggregated raster amounts are comparable
with the reference; relative-change rasters are not accepted directly.
Single definition and compatibility
Effects can be specified only once per problem. To compare effect scenarios,
create separate problems from a common object before calling
add_effects(). Historical delta, after,
multiplier, benefit, and loss inputs and explicit
effect_type, effect_aggregation, and component
arguments remain available with deprecation warnings. Use the three
modern tabular columns for new code.
An updated Problem containing dist_effects and
effects_meta; the stored effect table includes reference amounts,
action outcomes, signed effects, and compatibility columns.
# EXAMPLE 1: Equivalent ways to describe action effects # # Consider two planning units and two features: habitat and fuel load. # The reference amounts represent their values before implementing # any management action. # # In planning unit 1, habitat = 100 and fuel load = 60. # In planning unit 2, habitat = 40 and fuel load = 20. p <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1:2, name = c("habitat", "fuel")), dist_features = data.frame( pu = c(1, 1, 2, 2), feature = c(1, 2, 1, 2), amount = c(100, 60, 40, 20) ) ) |> add_actions(actions = data.frame(id = "restore")) # Suppose restoration increases habitat in planning unit 1 # from 100 to 130. This response can be described in three # equivalent ways: # # effect = +30 (absolute increase) # outcome = 130 (amount after restoration) # relative_change = 0.30 (30% increase) # # Each call starts from the same base problem because effects # can only be defined once per problem. p_effect <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", effect = 30 )) p_outcome <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", outcome = 130 )) p_relative <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", relative_change = 0.30 )) # Regardless of the input specification, multiscape stores # the reference amount, signed effect, and action outcome. p_effect$data$dist_effects[, c( "reference_amount", "effect", "action_outcome" )] # EXAMPLE 2: Effects that apply across planning units # # The 'pu' column is optional. When omitted, the specified # change is applied to every feasible planning unit for # the corresponding action. # # Here, restoration increases habitat by 25% in both units. # Because their reference amounts differ (100 and 40), # the resulting absolute effects also differ (+25 and +10). p_global <- add_effects(p, data.frame( action = "restore", feature = "habitat", relative_change = 0.25 )) # Inspect how the same proportional change produces # different absolute effects across planning units. p_global$data$dist_effects[, c( "pu", "reference_amount", "effect" )] # EXAMPLE 3: Positive and negative effects # # A management action may simultaneously increase some # features and decrease others. # # Suppose restoration increases habitat from 100 to 130 # but reduces fuel load from 60 to 45 in planning unit 1. # # Both responses are represented as signed effects: # # habitat: +30 # fuel: -15 # # Importantly, the sign describes the direction of change, # not whether that change is desirable. Increasing habitat # and reducing fuel load may both support management goals. p_mixed <- add_effects(p, data.frame( pu = 1, action = "restore", feature = c("habitat", "fuel"), effect = c(30, -15) )) # Inspect the direction and magnitude of each response. p_mixed$data$dist_effects[, c( "feature_name", "effect", "action_outcome" )] # EXAMPLE 4: Spatial variation in action outcomes # # Use the bundled 64-unit landscape to illustrate how # reference amounts and action outcomes vary spatially. # No optimisation or solver is required. # # The example applies a hypothetical 25% increase in the # first feature under the first available action. if (requireNamespace("sf", quietly = TRUE)) { # Load the spatial planning units, features, reference # distributions, and available management actions. sim <- load_sim_multiaction() spatial_problem <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # Select one feature and one management action. f <- sim$features$name[1] a <- sim$actions$id[1] # Define a hypothetical 25% increase relative to the # reference amount in every feasible planning unit. # Because reference amounts vary spatially, so do the # resulting absolute effects and action outcomes. spatial_problem <- add_effects( spatial_problem, data.frame( action = a, feature = f, relative_change = 0.25 ) ) # Extract the reference and expected outcome for the # selected feature-action combination. vals <- subset( spatial_problem$data$dist_effects, action == a & feature_name == f, select = c("pu", "reference_amount", "action_outcome") ) # Attach these values to the planning-unit geometries # so they can be displayed as spatial maps. mapped <- merge( sim$planning_units, vals, by.x = "id", by.y = "pu", all.x = TRUE ) # Compare reference amounts with potential outcomes. # These maps represent the consequences of implementing # the action, not an optimised selection of actions. plot(mapped[c("reference_amount", "action_outcome")]) } # EXAMPLE 5: Joint effects of multiple actions # # When multiple actions can be implemented together, # their combined effect need not equal the sum of their # individual effects. # # Consider two actions, 'restore' and 'control'. # Their individual effects on habitat are +30 and +20. # However, implementing both together produces a total # effect of +70 rather than +50. # # This implies an additional interaction of +20: # # restore effect = +30 # control effect = +20 # additional interaction = +20 # joint total effect = +70 # # The joint effect is supplied as the TOTAL change, # not merely the additional interaction. joint_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( list(restore_control = c("restore", "control")) ) |> add_constraint_action_cardinality(2, "max") # Supply individual and joint effects in the same call. # The combined action changes habitat from 100 to 170. joint <- add_effects(joint_base, data.frame( action = c("restore", "control", "restore_control"), feature = "habitat", effect = c(30, 20, 70) )) # Inspect the decomposition into individual effects and # the additional correction associated with joint selection. # The joint row stores a correction of +20, ensuring that # the combined total is +70 rather than +90. joint$data$effect_terms[, c( "action", "total_effect", "effect" )]# EXAMPLE 1: Equivalent ways to describe action effects # # Consider two planning units and two features: habitat and fuel load. # The reference amounts represent their values before implementing # any management action. # # In planning unit 1, habitat = 100 and fuel load = 60. # In planning unit 2, habitat = 40 and fuel load = 20. p <- create_problem( pu = data.frame(id = 1:2, cost = 0), features = data.frame(id = 1:2, name = c("habitat", "fuel")), dist_features = data.frame( pu = c(1, 1, 2, 2), feature = c(1, 2, 1, 2), amount = c(100, 60, 40, 20) ) ) |> add_actions(actions = data.frame(id = "restore")) # Suppose restoration increases habitat in planning unit 1 # from 100 to 130. This response can be described in three # equivalent ways: # # effect = +30 (absolute increase) # outcome = 130 (amount after restoration) # relative_change = 0.30 (30% increase) # # Each call starts from the same base problem because effects # can only be defined once per problem. p_effect <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", effect = 30 )) p_outcome <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", outcome = 130 )) p_relative <- add_effects(p, data.frame( pu = 1, action = "restore", feature = "habitat", relative_change = 0.30 )) # Regardless of the input specification, multiscape stores # the reference amount, signed effect, and action outcome. p_effect$data$dist_effects[, c( "reference_amount", "effect", "action_outcome" )] # EXAMPLE 2: Effects that apply across planning units # # The 'pu' column is optional. When omitted, the specified # change is applied to every feasible planning unit for # the corresponding action. # # Here, restoration increases habitat by 25% in both units. # Because their reference amounts differ (100 and 40), # the resulting absolute effects also differ (+25 and +10). p_global <- add_effects(p, data.frame( action = "restore", feature = "habitat", relative_change = 0.25 )) # Inspect how the same proportional change produces # different absolute effects across planning units. p_global$data$dist_effects[, c( "pu", "reference_amount", "effect" )] # EXAMPLE 3: Positive and negative effects # # A management action may simultaneously increase some # features and decrease others. # # Suppose restoration increases habitat from 100 to 130 # but reduces fuel load from 60 to 45 in planning unit 1. # # Both responses are represented as signed effects: # # habitat: +30 # fuel: -15 # # Importantly, the sign describes the direction of change, # not whether that change is desirable. Increasing habitat # and reducing fuel load may both support management goals. p_mixed <- add_effects(p, data.frame( pu = 1, action = "restore", feature = c("habitat", "fuel"), effect = c(30, -15) )) # Inspect the direction and magnitude of each response. p_mixed$data$dist_effects[, c( "feature_name", "effect", "action_outcome" )] # EXAMPLE 4: Spatial variation in action outcomes # # Use the bundled 64-unit landscape to illustrate how # reference amounts and action outcomes vary spatially. # No optimisation or solver is required. # # The example applies a hypothetical 25% increase in the # first feature under the first available action. if (requireNamespace("sf", quietly = TRUE)) { # Load the spatial planning units, features, reference # distributions, and available management actions. sim <- load_sim_multiaction() spatial_problem <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) # Select one feature and one management action. f <- sim$features$name[1] a <- sim$actions$id[1] # Define a hypothetical 25% increase relative to the # reference amount in every feasible planning unit. # Because reference amounts vary spatially, so do the # resulting absolute effects and action outcomes. spatial_problem <- add_effects( spatial_problem, data.frame( action = a, feature = f, relative_change = 0.25 ) ) # Extract the reference and expected outcome for the # selected feature-action combination. vals <- subset( spatial_problem$data$dist_effects, action == a & feature_name == f, select = c("pu", "reference_amount", "action_outcome") ) # Attach these values to the planning-unit geometries # so they can be displayed as spatial maps. mapped <- merge( sim$planning_units, vals, by.x = "id", by.y = "pu", all.x = TRUE ) # Compare reference amounts with potential outcomes. # These maps represent the consequences of implementing # the action, not an optimised selection of actions. plot(mapped[c("reference_amount", "action_outcome")]) } # EXAMPLE 5: Joint effects of multiple actions # # When multiple actions can be implemented together, # their combined effect need not equal the sum of their # individual effects. # # Consider two actions, 'restore' and 'control'. # Their individual effects on habitat are +30 and +20. # However, implementing both together produces a total # effect of +70 rather than +50. # # This implies an additional interaction of +20: # # restore effect = +30 # control effect = +20 # additional interaction = +20 # joint total effect = +70 # # The joint effect is supplied as the TOTAL change, # not merely the additional interaction. joint_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( list(restore_control = c("restore", "control")) ) |> add_constraint_action_cardinality(2, "max") # Supply individual and joint effects in the same call. # The combined action changes habitat from 100 to 170. joint <- add_effects(joint_base, data.frame( action = c("restore", "control", "restore_control"), feature = "habitat", effect = c(30, 20, 70) )) # Inspect the decomposition into individual effects and # the additional correction associated with joint selection. # The joint row stores a correction of +20, ensuring that # the combined total is +70 rather than +90. joint$data$effect_terms[, c( "action", "total_effect", "effect" )]
Legacy convenience wrapper around add_effects that retains
only negative signed changes (effect < 0). Negative change does not
necessarily imply an ecological loss. For new code, prefer
add_effects() to retain signed effects of both directions.
Effects can be defined only once per problem.
add_losses( x, losses = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean") )add_losses( x, losses = NULL, effect_type = c("delta", "after"), effect_aggregation = c("sum", "mean") )
x |
A |
losses |
Alias of |
effect_type |
Deprecated interpretation argument: |
effect_aggregation |
Deprecated raster aggregation argument; use
|
An updated Problem object containing:
dist_effectsThe canonical filtered effects table,
containing only rows with loss > 0.
dist_lossA convenience table containing only the loss component.
losses_metaMetadata for the stored loss table.
add_effects,
add_benefits,
add_objective_min_loss
Use add_objective_max_effect() for signed net effects.
add_objective_max_benefit(x, actions = NULL, features = NULL, alias = NULL)add_objective_max_benefit(x, actions = NULL, features = NULL, alias = NULL)
x |
A |
actions |
Optional subset of actions to include in the objective. Values
may match |
features |
Optional subset of features to include in the objective.
Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
An updated Problem object.
Maximize the total signed change generated by selected management actions relative to the reference scenario. Positive changes increase the objective; negative changes reduce it.
add_objective_max_effect(x, actions = NULL, features = NULL, alias = NULL)add_objective_max_effect(x, actions = NULL, features = NULL, alias = NULL)
x |
A |
actions |
Optional subset of actions to include in the objective. Values
may match |
features |
Optional subset of features to include in the objective.
Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
Let be the signed change for planning unit and
feature , after combining selected individual actions and their
interaction corrections. The objective is
For one action per unit this reduces to the sum of selected signed effects. With concurrent actions, supplied joint totals and inferred combinations are evaluated together; a negative interaction correction is not discarded.
The reference is the scenario supplied through dist_features, at the
same horizon and in the same units as action outcomes. An unselected unit
contributes zero change. Costs are not subtracted by this objective.
A positive effect means an increase in the feature; select features for which
an increase is desirable. Standardize incompatible units before aggregation,
or register separate feature objectives using distinct aliases.
With actions, include only individual terms in that subset and joint
corrections whose every member belongs to it. This is an accounting scope,
not attribution of a cross-subset interaction to its individual members.
With features, include only the selected features.
An updated Problem object.
This development version maximizes signed net change. Earlier versions
maximized positive effects only. Models with negative effects or negative
interaction corrections may therefore produce different solutions. The
historical add_objective_max_benefit() name is deprecated in favor of
add_objective_max_effect(). get_features() still reports positive gains separately in
selected_benefit; the signed objective corresponds to
selected_net when their scopes match.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Tallis, H., et al. (2021). Prioritizing actions: spatial action maps for conservation. Annals of the New York Academy of Sciences, 1505, 118–141. doi:10.1111/nyas.14651
Salgado-Rojas, J., Álvarez-Miranda, E., Hermoso, V., Garcia-Gonzalo, J., and Weintraub, A. (2020). A mixed integer programming approach for multi-action planning for threat management. Ecological Modelling, 418, 108901. doi:10.1016/j.ecolmodel.2019.108901
add_objective_min_effect,
add_effects
# EXAMPLE: Maximise woodland gain in a fixed number of units # # The bundled landscape has spatially varying woodland reference # amounts. Assume restoration increases these amounts by 50%. # The absolute signed effect is therefore larger where the # reference amount is greater. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_effects(data.frame( action = "restore", feature = "woodland", relative_change = 0.50 )) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_max_effect(features = "woodland", alias = "woodland_gain") # Every restoration costs one monetary unit, so exactly 12 units # must be managed. The objective favours the largest woodland gains. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Maximise woodland gain in a fixed number of units # # The bundled landscape has spatially varying woodland reference # amounts. Assume restoration increases these amounts by 50%. # The absolute signed effect is therefore larger where the # reference amount is greater. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_effects(data.frame( action = "restore", feature = "woodland", relative_change = 0.50 )) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_max_effect(features = "woodland", alias = "woodland_gain") # Every restoration costs one monetary unit, so exactly 12 units # must be managed. The objective favours the largest woodland gains. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Define an objective that maximizes net profit by combining profits with optional planning-unit and action-cost penalties.
add_objective_max_net_profit( x, profit_col = "profit", include_pu_cost = TRUE, include_action_cost = TRUE, actions = NULL, alias = NULL )add_objective_max_net_profit( x, profit_col = "profit", include_pu_cost = TRUE, include_action_cost = TRUE, actions = NULL, alias = NULL )
x |
A |
profit_col |
Character string giving the profit column in the stored profit table. |
include_pu_cost |
Logical. If |
include_action_cost |
Logical. If |
actions |
Optional subset of actions to include in the profit and
action-cost terms. Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
Use this function when decisions generate returns and the objective should optimize the resulting net balance after subtracting selected cost components.
Let:
denote whether action is selected
in planning unit ,
denote whether planning unit is
selected,
denote the profit associated with decision
,
denote the planning-unit cost,
denote the action cost.
In its most general form, the objective is:
where denotes the subset of feasible
planning unit–action decisions included in the objective.
If actions = NULL, all feasible actions contribute to both the profit
term and the action-cost term.
If actions is provided, the profit term and the action-cost term are
restricted to that subset. The planning-unit cost term, if included, remains
global.
If include_pu_cost = FALSE, the planning-unit cost term is omitted.
If include_action_cost = FALSE, the action-cost term is omitted.
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Polasky, S., et al. (2008). Where to put things? Spatial land management to sustain biodiversity and economic returns. Biological Conservation, 141, 1505–1524. doi:10.1016/j.biocon.2008.03.022
Chan, K. M. A., Hoshizaki, L., and Klinkenberg, B. (2011). Ecosystem services in conservation planning: targeted benefits vs. co-benefits or costs? PLOS ONE, 6, e24378. doi:10.1371/journal.pone.0024378
add_objective_max_profit,
add_objective_min_cost
# EXAMPLE: Maximise returns minus implementation costs # # Use the same west-to-east profit pattern as above, but now # charge 9.5 monetary units for every selected action. Some # locations may no longer generate a positive net return. sim <- load_sim_multiaction() returns <- sim$action_costs[, c("pu", "action")] x_coord <- sim$planning_units$x[ match(returns$pu, sim$planning_units$id) ] returns$profit <- ifelse( returns$action == "protect", 12 - x_coord, 4 + x_coord ) p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 9.5) |> add_profit(returns) |> add_objective_max_net_profit(include_pu_cost = FALSE, alias = "net_profit") # Unlike gross profit maximisation, leaving a unit unmanaged # can now be optimal when its best return is below its cost. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Maximise returns minus implementation costs # # Use the same west-to-east profit pattern as above, but now # charge 9.5 monetary units for every selected action. Some # locations may no longer generate a positive net return. sim <- load_sim_multiaction() returns <- sim$action_costs[, c("pu", "action")] x_coord <- sim$planning_units$x[ match(returns$pu, sim$planning_units$id) ] returns$profit <- ifelse( returns$action == "protect", 12 - x_coord, 4 + x_coord ) p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 9.5) |> add_profit(returns) |> add_objective_max_net_profit(include_pu_cost = FALSE, alias = "net_profit") # Unlike gross profit maximisation, leaving a unit unmanaged # can now be optimal when its best return is below its cost. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Define an objective that maximizes total profit from selected planning unit–action decisions.
add_objective_max_profit( x, profit_col = "profit", actions = NULL, alias = NULL )add_objective_max_profit( x, profit_col = "profit", actions = NULL, alias = NULL )
x |
A |
profit_col |
Character string giving the profit column in the stored profit table. |
actions |
Optional subset of actions to include. Values may match
|
alias |
Optional identifier used to register this objective for multi-objective workflows. |
Use this function when the objective is to maximize gross economic return, without subtracting planning-unit or action costs.
Let denote whether action is selected in
planning unit , and let denote the profit associated
with that decision, as taken from column profit_col in the stored
profit table.
If all actions are included, the objective is:
where denotes the set of feasible planning unit–action
decisions.
If actions is provided, only the selected subset contributes to the
objective. Letting denote the feasible decisions
whose action belongs to the selected subset, the objective becomes:
This objective considers profit only. It does not subtract planning-unit
costs or action costs. For a net-profit formulation, use
add_objective_max_net_profit.
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Polasky, S., et al. (2008). Where to put things? Spatial land management to sustain biodiversity and economic returns. Biological Conservation, 141, 1505–1524. doi:10.1016/j.biocon.2008.03.022
add_objective_min_cost,
add_objective_max_net_profit
# EXAMPLE: Maximise gross economic return across the landscape # # Assign hypothetical profits that favour protection in the west # and restoration in the east. All values are positive. sim <- load_sim_multiaction() returns <- sim$action_costs[, c("pu", "action")] x_coord <- sim$planning_units$x[ match(returns$pu, sim$planning_units$id) ] returns$profit <- ifelse( returns$action == "protect", 12 - x_coord, 4 + x_coord ) p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 9.5) |> add_profit(returns) |> add_objective_max_profit(alias = "profit") # Implementation costs are NOT deducted in this objective. # With positive returns and at most one action per unit, the # model chooses the more profitable action in each unit. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Maximise gross economic return across the landscape # # Assign hypothetical profits that favour protection in the west # and restoration in the east. All values are positive. sim <- load_sim_multiaction() returns <- sim$action_costs[, c("pu", "action")] x_coord <- sim$planning_units$x[ match(returns$pu, sim$planning_units$id) ] returns$profit <- ifelse( returns$action == "protect", 12 - x_coord, 4 + x_coord ) p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 9.5) |> add_profit(returns) |> add_objective_max_profit(alias = "profit") # Implementation costs are NOT deducted in this objective. # With positive returns and at most one action per unit, the # model chooses the more profitable action in each unit. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Define an objective that minimizes the total cost of the solution.
Depending on the function arguments, the objective may include planning-unit costs, action costs, or both. Action costs can optionally be restricted to a subset of actions.
add_objective_min_cost( x, include_pu_cost = TRUE, include_action_cost = TRUE, actions = NULL, alias = NULL )add_objective_min_cost( x, include_pu_cost = TRUE, include_action_cost = TRUE, actions = NULL, alias = NULL )
x |
A |
include_pu_cost |
Logical. If |
include_action_cost |
Logical. If |
actions |
Optional subset of actions to include in the action-cost
component. Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
Use this function when the planning problem is framed primarily as a cost-minimization problem, with costs arising from planning-unit selection, action implementation, or both.
Let be the set of planning units and let
denote the set of
feasible planning unit–action decisions.
Let:
denote whether planning unit is
selected,
denote whether action is selected
in planning unit ,
denote the planning-unit cost of unit ,
denote the cost of selecting action
in planning unit .
The most general form of this objective is:
where denotes the subset of feasible decisions
whose action contributes to the action-cost term.
If include_pu_cost = FALSE, the planning-unit cost term is omitted.
If include_action_cost = FALSE, the action-cost term is omitted.
If actions = NULL, all feasible actions contribute to the action-cost
term. If actions is supplied, only the selected subset contributes to
that term. Planning-unit costs are never subset by actions; they are
always global whenever include_pu_cost = TRUE.
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Ball, I. R., Possingham, H. P., and Watts, M. E. (2009). Marxan and relatives: software for spatial conservation prioritisation. In Spatial Conservation Prioritization: Quantitative Methods and Computational Tools, pp. 185–195. doi:10.1093/oso/9780199547760.003.0014
Salgado-Rojas, J., Hermoso, V., and Álvarez-Miranda, E. (2025). prioriactions: multi-action management planning in R. Methods in Ecology and Evolution, 16, 1028–1039. doi:10.1111/2041-210X.14220
add_objective_max_profit,
add_objective_max_net_profit
# EXAMPLE: Minimum-cost management with a woodland target # # Use the 64 planning-unit landscape. The hypothetical outcome of 1 # credits one unit of woodland for either action in each selected unit. # A target of 12 therefore requires at least 12 interventions. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(data.frame( action = c("protect", "restore"), feature = "woodland", outcome = 1 )) |> add_constraint_targets_absolute(12, features = "woodland") |> add_objective_min_cost(include_pu_cost = FALSE, alias = "cost") # The solver selects the least costly set of actions that meets the target. # This is a single-objective model: no set_method_*() call is needed. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Minimum-cost management with a woodland target # # Use the 64 planning-unit landscape. The hypothetical outcome of 1 # credits one unit of woodland for either action in each selected unit. # A target of 12 therefore requires at least 12 interventions. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(data.frame( action = c("protect", "restore"), feature = "woodland", outcome = 1 )) |> add_constraint_targets_absolute(12, features = "woodland") |> add_objective_min_cost(include_pu_cost = FALSE, alias = "cost") # The solver selects the least costly set of actions that meets the target. # This is a single-objective model: no set_method_*() call is needed. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Minimise total signed changes relative to the reference. Whether a decrease is desirable depends on the feature being optimised.
add_objective_min_effect(x, actions = NULL, features = NULL, alias = NULL)add_objective_min_effect(x, actions = NULL, features = NULL, alias = NULL)
x |
A |
actions |
Optional subset of actions to include in the objective. Values
may match |
features |
Optional subset of features to include in the objective.
Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
For the selected actions and features, this objective minimises
, where
is the signed change after accounting for joint effects. Increases
and decreases can offset one another across units or features.
For instance, minimising signed fuel-load change favours reductions in fuel load. Specify a feature subset when features are measured in incompatible units. Unselected units contribute zero change, and implementation costs are not subtracted.
This differs from the deprecated add_objective_min_loss(),
which penalises only the negative part of aggregated changes.
An updated Problem object.
Related spatial fuel-management applications (not necessarily using the identical signed-effect objective) include:
Minas, J. P., Hearne, J. W., and Martell, D. L. (2014). A spatial optimisation model for multi-period landscape level fuel management to mitigate wildfire impacts. European Journal of Operational Research, 232, 412–422. doi:10.1016/j.ejor.2013.07.026
Williams, B. A., Shoo, L. P., Wilson, K. A., and Beyer, H. L. (2017). Optimising the spatial planning of prescribed burns to achieve multiple objectives in a fire-dependent ecosystem. Journal of Applied Ecology. doi:10.1111/1365-2664.12920
add_objective_max_effect(), add_objective_min_loss()
# EXAMPLE: Minimise the signed change in fuel load # # Use the geometry from the bundled 64-unit landscape, with a simple # hypothetical fuel-load feature that increases from west to east. # A 25% reduction has a NEGATIVE signed effect, so minimising the # effect favours the units with the greatest fuel-load reduction. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = data.frame(id = 1L, name = "fuel_load"), dist_features = data.frame( pu = sim$planning_units$id, feature = 1L, amount = 10 + sim$planning_units$x ), cost = "cost" ) |> add_actions(data.frame(id = "thin"), cost = 1) |> add_effects(data.frame( action = "thin", feature = "fuel_load", relative_change = -0.25 )) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_min_effect(features = "fuel_load", alias = "fuel_reduction") # With one monetary unit per action, the budget equality selects # exactly 12 units. Without it, this negative-effect objective # could favour implementing the action everywhere. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Minimise the signed change in fuel load # # Use the geometry from the bundled 64-unit landscape, with a simple # hypothetical fuel-load feature that increases from west to east. # A 25% reduction has a NEGATIVE signed effect, so minimising the # effect favours the units with the greatest fuel-load reduction. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = data.frame(id = 1L, name = "fuel_load"), dist_features = data.frame( pu = sim$planning_units$id, feature = 1L, amount = 10 + sim$planning_units$x ), cost = "cost" ) |> add_actions(data.frame(id = "thin"), cost = 1) |> add_effects(data.frame( action = "thin", feature = "fuel_load", relative_change = -0.25 )) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_min_effect(features = "fuel_load", alias = "fuel_reduction") # With one monetary unit per action, the budget equality selects # exactly 12 units. Without it, this negative-effect objective # could favour implementing the action everywhere. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Encourage spatial cohesion separately for each selected action, rather than only for the union of managed planning units.
add_objective_min_fragmentation_action( x, relation_name = "boundary", weight_multiplier = 1, action_weights = NULL, actions = NULL, alias = NULL )add_objective_min_fragmentation_action( x, relation_name = "boundary", weight_multiplier = 1, action_weights = NULL, actions = NULL, alias = NULL )
x |
A |
relation_name |
Character string giving the name of the spatial relation
to use. The relation must already exist in
|
weight_multiplier |
Numeric scalar greater than or equal to zero. Global multiplier applied to the relation weights when the objective is built. |
action_weights |
Optional action weights. Either a named numeric vector
with names equal to action ids, or a |
actions |
Optional subset of actions to include. Values may match
|
alias |
Optional identifier used to register this objective for multi-objective workflows. |
This objective uses the action-selection decisions and
a previously registered spatial relation. For neighbouring units
and , represents
whether the same action occurs in both units. Adjacency of
different actions does not form a continuous patch of either action.
For each action, the relation is restricted automatically to its feasible
planning-unit/action pairs (as supplied through include_pairs and
retained in the compiled model). An off-diagonal link contributes only if
that action is feasible at both endpoints. A feasible but unselected action
remains in this domain. No extra argument or correction objective is needed.
Self-boundary weights contribute only where that action is feasible.
Use actions to select which action patterns contribute,
action_weights to adjust their relative importance, and
weight_multiplier to scale spatial relation weights.
Unlike add_objective_min_fragmentation_planning_units(),
this objective distinguishes action identities. Combine it with
coverage or allocation requirements so an empty plan is not optimal.
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Salgado-Rojas, J., Álvarez-Miranda, E., Hermoso, V., Garcia-Gonzalo, J., and Weintraub, A. (2020). A mixed integer programming approach for multi-action planning for threat management. Ecological Modelling, 418, 108901. doi:10.1016/j.ecolmodel.2019.108901
Álvarez-Miranda, E., Salgado-Rojas, J., Hermoso, V., Garcia-Gonzalo, J., and Weintraub, A. (2020). An integer programming method for the design of multi-criteria multi-action conservation plans. Omega, 92, 102147. doi:10.1016/j.omega.2019.102147
add_objective_min_fragmentation_planning_units,
add_spatial_boundary,
add_spatial_relations
# EXAMPLE: Form separate cohesive patches for two actions # # Each action costs one unit. Two action-specific budget equalities # require exactly four protection and four restoration units. # The default one-action-per-unit rule prevents overlapping actions. sim <- load_sim_multiaction() # Use a 4 by 4 subset so this example solves quickly. planning_units <- subset(sim$planning_units, x < 4 & y < 4) dist_features <- subset(sim$dist_features, pu %in% planning_units$id) p <- create_problem( pu = planning_units, features = sim$features, dist_features = dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 1) |> add_spatial_boundary(name = "boundary", include_self = TRUE) |> add_constraint_budget( 4, "equal", actions = "protect", include_pu_cost = FALSE ) |> add_constraint_budget( 4, "equal", actions = "restore", include_pu_cost = FALSE ) |> add_objective_min_fragmentation_action(relation_name = "boundary", alias = "action_fragmentation") # Unlike planning-unit fragmentation, this objective measures # the cohesion of each action's selected units separately. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, gap_limit = 0, time_limit = 5, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }# EXAMPLE: Form separate cohesive patches for two actions # # Each action costs one unit. Two action-specific budget equalities # require exactly four protection and four restoration units. # The default one-action-per-unit rule prevents overlapping actions. sim <- load_sim_multiaction() # Use a 4 by 4 subset so this example solves quickly. planning_units <- subset(sim$planning_units, x < 4 & y < 4) dist_features <- subset(sim$dist_features, pu %in% planning_units$id) p <- create_problem( pu = planning_units, features = sim$features, dist_features = dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = 1) |> add_spatial_boundary(name = "boundary", include_self = TRUE) |> add_constraint_budget( 4, "equal", actions = "protect", include_pu_cost = FALSE ) |> add_constraint_budget( 4, "equal", actions = "restore", include_pu_cost = FALSE ) |> add_objective_min_fragmentation_action(relation_name = "boundary", alias = "action_fragmentation") # Unlike planning-unit fragmentation, this objective measures # the cohesion of each action's selected units separately. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, gap_limit = 0, time_limit = 5, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_actions(solutions, layout = "single") }
Encourage cohesion of the selected planning units, regardless of which action is implemented within each unit.
add_objective_min_fragmentation_planning_units( x, relation_name = "boundary", weight_multiplier = 1, alias = NULL )add_objective_min_fragmentation_planning_units( x, relation_name = "boundary", weight_multiplier = 1, alias = NULL )
x |
A |
relation_name |
Character string giving the name of the spatial relation
to use. The relation must already exist in
|
weight_multiplier |
Numeric scalar greater than or equal to zero. Global multiplier applied to the relation weights when the objective is built. |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
This objective uses planning-unit selection variables and
a spatial relation registered with add_spatial_relations() or
add_spatial_boundary(). Relation weights
are scaled by weight_multiplier.
The underlying model uses to record
whether neighbouring units are both selected, encouraging spatially
consolidated selections. Action identities do not enter this spatial
criterion: adjacent units receiving different actions are part of the
same selected planning-unit set.
Without another requirement, selecting no units can be optimal.
Combine this objective with a budget equality, an area requirement,
or an ecological target that ensures meaningful management activity.
For action-specific cohesion, see
add_objective_min_fragmentation_action().
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
Ball, I. R., Possingham, H. P., and Watts, M. E. (2009). Marxan and relatives: software for spatial conservation prioritisation. In Spatial Conservation Prioritization: Quantitative Methods and Computational Tools, pp. 185–195. doi:10.1093/oso/9780199547760.003.0014
Önal, H., and Briers, R. A. (2002). Incorporating spatial criteria in optimum reserve network selection. Proceedings of the Royal Society B, 269, 2437–2441. doi:10.1098/rspb.2002.2183
add_spatial_boundary,
add_spatial_relations,
add_objective_min_fragmentation_action,
add_objective_min_fragmentation_pu
# EXAMPLE: Select a cohesive set of 12 planning units # # With one feasible action costing one unit, a budget equality # forces exactly 12 selected units. Without this requirement, # minimising fragmentation alone could select no units. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_spatial_boundary(name = "boundary", include_self = TRUE) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_min_fragmentation_planning_units( relation_name = "boundary", alias = "pu_fragmentation" ) # Cohesion is evaluated for the selected planning-unit pattern, # irrespective of action identity. No additional objective is used. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_planning_units(solutions) }# EXAMPLE: Select a cohesive set of 12 planning units # # With one feasible action costing one unit, a budget equality # forces exactly 12 selected units. Without this requirement, # minimising fragmentation alone could select no units. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_spatial_boundary(name = "boundary", include_self = TRUE) |> add_constraint_budget(12, "equal", include_pu_cost = FALSE) |> add_objective_min_fragmentation_planning_units( relation_name = "boundary", alias = "pu_fragmentation" ) # Cohesion is evaluated for the selected planning-unit pattern, # irrespective of action identity. No additional objective is used. if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve(set_solver_cbc(p, time_limit = 30, verbose = FALSE)) get_objectives(solutions, format = "wide") plot_spatial_planning_units(solutions) }
add_objective_min_fragmentation_pu() has been replaced by
add_objective_min_fragmentation_planning_units.
add_objective_min_fragmentation_pu( x, relation_name = "boundary", weight_multiplier = 1, alias = NULL )add_objective_min_fragmentation_pu( x, relation_name = "boundary", weight_multiplier = 1, alias = NULL )
x |
A |
relation_name |
Character string giving the name of the spatial relation
to use. The relation must already exist in
|
weight_multiplier |
Numeric scalar greater than or equal to zero. Global multiplier applied to the relation weights when the objective is built. |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
An updated Problem object.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
add_objective_min_fragmentation_planning_units
Deprecated; retained temporarily with its original behavior.
Define an objective that minimizes the impact associated with selecting planning units for intervention.
This objective uses planning-unit selection variables rather than summing the same impact repeatedly over multiple actions. As a result, each planning unit contributes at most once to the objective, regardless of how many feasible actions exist in that unit.
add_objective_min_intervention_impact( x, impact_col = "amount", features = NULL, actions = NULL, alias = NULL )add_objective_min_intervention_impact( x, impact_col = "amount", features = NULL, actions = NULL, alias = NULL )
x |
A |
impact_col |
Character string giving the column in the
feature-distribution table that contains the per- |
features |
Optional subset of features to include. Values may match
|
actions |
Optional subset of actions used to define the intervention
context. Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
The legacy objective minimizes the reference amount in units where at least one action in the selected scope is executed, counting each unit once. Its original arguments, objective sense, and single/MO formulations remain available during the transition.
An updated Problem object.
This function will be removed in a future version of multiscape. New workflows
should express action consequences with add_effects and optimize
signed changes with add_objective_max_effect.
This is not an automatic replacement. For deficit-based prioritization,
let be the reference amount and a common ceiling for
feature . Supply as the restoration effect, or
as its outcome. If every plan restores exactly units and
at most one scoped action is selected in each unit, then
Maximizing this deficit is equivalent to minimizing the old baseline sum.
Equal area fixes only when units have equal effective action areas.
With variable effort, varying ceilings, or concurrent scoped actions,
equivalence is not guaranteed. The common ceiling is a modelling assumption,
not an empirical estimate of restoration response.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
add_objective_max_effect,
add_objective_min_loss
# Equal-area units with a fixed restoration effort of two units. p <- create_problem( data.frame(id = 1:3, cost = 1, area = 1), data.frame(id = 1, name = "service"), data.frame(pu = 1:3, feature = 1, amount = c(0.2, 0.6, 0.9)) ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_effects(data.frame(action = "restore", feature = "service", outcome = 1)) |> add_constraint_area(2, "equal", tolerance = 0, actions = "restore") |> add_objective_max_effect(features = "service", actions = "restore") p$data$model_args# Equal-area units with a fixed restoration effort of two units. p <- create_problem( data.frame(id = 1:3, cost = 1, area = 1), data.frame(id = 1, name = "service"), data.frame(pu = 1:3, feature = 1, amount = c(0.2, 0.6, 0.9)) ) |> add_actions(data.frame(id = "restore"), cost = 1) |> add_effects(data.frame(action = "restore", feature = "service", outcome = 1)) |> add_constraint_area(2, "equal", tolerance = 0, actions = "restore") |> add_objective_max_effect(features = "service", actions = "restore") p$data$model_args
Minimize deterioration relative to the reference scenario. Positive changes in other planning units or features do not offset these losses.
add_objective_min_loss(x, actions = NULL, features = NULL, alias = NULL)add_objective_min_loss(x, actions = NULL, features = NULL, alias = NULL)
x |
A |
actions |
Optional subset of actions to include in the objective. Values
may match |
features |
Optional subset of features to include in the objective.
Values may match |
alias |
Optional identifier used to register this objective for multi-objective workflows. |
Let be the signed change for planning unit and
feature , after combining selected individual actions and their
interaction corrections. The objective is
The positive/negative split is applied after joint aggregation within each unit and feature, before summing across units or features. A negative interaction correction does not itself represent a loss if the final change remains positive. Conversely, gains elsewhere cannot cancel deterioration.
With one action per unit this retains the existing loss-only criterion. With concurrent actions, an exact MILP linearization represents mixed-sign final losses in single-objective and all multi-objective methods, including when loss is constrained, has zero weight, or is evaluated after another objective is optimized. Non-negative effects give a valid zero-loss objective.
Action and feature subsets follow add_objective_max_benefit(): an
interaction is included only when all of its members belong to the scope.
An unselected unit contributes zero loss. Use targets or other objectives if
avoiding intervention altogether should not be an acceptable solution.
An updated Problem object.
Deprecated since 1.4.0. Legacy calls retain the negative-part criterion. add_objective_min_effect() is not a mathematically equivalent replacement.
With alias = NULL, one explicit single objective can be defined per
problem; a second unaliased definition raises an error. With an alias,
objectives accumulate under distinct names; a repeated alias raises an error.
Aliased definitions preserve an explicitly configured single objective.
To compare alternatives, start from the problem before its objective was added.
add_objective_max_benefit,
add_effects
# 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 ) |> add_effects( example_data$effect_assumptions ) p1 <- add_objective_min_loss(p) p1$data$model_args p2 <- add_objective_min_loss( p, actions = "restore" ) p2$data$model_args p3 <- add_objective_min_loss( p, features = 1 ) p3$data$model_args# 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 ) |> add_effects( example_data$effect_assumptions ) p1 <- add_objective_min_loss(p) p1$data$model_args p2 <- add_objective_min_loss( p, actions = "restore" ) p2$data$model_args p3 <- add_objective_min_loss( p, features = 1 ) p3$data$model_args
Assign economic returns to feasible planning-unit–action pairs. Returns may be positive, zero, or negative and are stored separately from implementation costs and ecological effects.
add_profit(x, profit = NULL)add_profit(x, profit = NULL)
x |
A |
profit |
Profit specification. One of:
Profit can be defined only once per problem, including explicit zero profit. A second call raises an error. To compare profit scenarios, build separate problems from the object before profit was added. |
Economic returns and implementation costs
An economic return is the monetary contribution associated
with selecting action in planning unit . Positive values
represent gains or revenues; negative values represent penalties or
economic losses. A zero value contributes no profit.
Profit is registered independently of planning-unit and action costs.
add_objective_max_profit() maximises the sum of registered returns,
whereas add_objective_max_net_profit() can subtract implementation
costs. If your profit inputs already account for these costs, avoid
subtracting them again when configuring the objective. Economic returns
should not be confused with the signed ecological changes registered
using add_effects().
Supported input formats
The profit argument accepts:
NULL: zero profit for all feasible pairs;
one numeric value: the same profit for every feasible pair;
a named numeric vector: one profit value per named action;
data.frame(action, profit): one profit value per action;
data.frame(pu, action, profit): values specific to
individual planning-unit–action pairs.
An action-level value applies to all feasible planning units for that action. A pair-specific table permits spatially varying returns. Feasible pairs not specified in the input receive profit zero.
Storage and use in planning
Only non-zero values are stored in dist_profit. Zero-profit pairs
are omitted from the table but remain feasible. The stored columns are
pu, action, profit, internal_pu, and
internal_action. Calling add_profit() neither selects actions
nor modifies feasible actions or ecological effects; the values are used
later by objectives, constraints, and solution summaries.
Profit can be defined only once per problem, including when supplying
NULL. To compare economic scenarios, start from the same problem
before profit was added.
An updated Problem object with a stored profit table created.
The stored table contains columns pu, action,
profit, internal_pu, and internal_action, and
includes only rows with non-zero profit.
add_actions,
add_objective_max_profit,
add_objective_max_net_profit,
add_effects
# EXAMPLE 1: Create a planning problem with feasible actions # The bundled 64-unit landscape has protection and restoration # alternatives. Action implementation costs are registered separately # from the economic returns introduced below. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions( actions = sim$actions, cost = sim$action_costs ) # EXAMPLE 2: A constant economic return # Suppose implementing any feasible action generates 10 monetary units, # regardless of the action or its location. A scalar is applied to all # feasible planning-unit--action pairs. p_constant <- add_profit(p, profit = 10) head(p_constant$data$dist_profit[, c("pu", "action", "profit")]) # EXAMPLE 3: Action-specific returns # Protection generates +50 monetary units, whereas restoration incurs # an economic penalty of -5 units. A named vector assigns a value to # every feasible planning unit where that action is available. returns <- c(protect = 50, restore = -5) p_action <- add_profit(p, profit = returns) head(p_action$data$dist_profit[, c("pu", "action", "profit")]) # The same action-level values can be supplied as a data frame. # These alternatives start from the same base problem because profit # can be defined only once per problem. p_action_table <- add_profit(p, data.frame( action = c("protect", "restore"), profit = c(50, -5) )) head(p_action_table$data$dist_profit[, c("pu", "action", "profit")]) # EXAMPLE 4: Location-specific economic returns # The same action can have different returns across planning units. # Protection provides +100 in unit 1 and +80 in unit 2; restoration # provides +30 in unit 3. Unlisted feasible pairs receive zero profit. local_returns <- data.frame( pu = c(1, 2, 3), action = c("protect", "protect", "restore"), profit = c(100, 80, 30) ) p_local <- add_profit(p, profit = local_returns) # Only the three non-zero contributions are stored. This does not # make any of the other, zero-profit action pairs infeasible. p_local$data$dist_profit[, c("pu", "action", "profit")] # EXAMPLE 5: Explicitly specify no economic return # NULL assigns zero to every feasible pair. The resulting sparse # profit table is empty, even though the actions remain available. p_zero <- add_profit(p, profit = NULL) nrow(p_zero$data$dist_profit) # EXAMPLE 6: Map spatially varying potential returns # The simulated planning units include geometry and an x-coordinate. # Give restoration a hypothetical return that increases from west to # east. These are potential returns, not selected management actions. # No optimiser or solver is needed to visualise the input values. if (requireNamespace("sf", quietly = TRUE)) { spatial_returns <- data.frame( pu = sim$planning_units$id, action = "restore", profit = 20 + 5 * sim$planning_units$x ) p_spatial <- add_profit(p, profit = spatial_returns) # Attach the registered restoration returns to the geometry. values <- subset( p_spatial$data$dist_profit, action == "restore", select = c("pu", "profit") ) mapped <- merge( sim$planning_units, values, by.x = "id", by.y = "pu", all.x = TRUE ) # This map describes where restoration could generate economic # returns, not where optimisation has selected restoration. plot(mapped["profit"], main = "Potential economic return from restoration") }# EXAMPLE 1: Create a planning problem with feasible actions # The bundled 64-unit landscape has protection and restoration # alternatives. Action implementation costs are registered separately # from the economic returns introduced below. sim <- load_sim_multiaction() p <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions( actions = sim$actions, cost = sim$action_costs ) # EXAMPLE 2: A constant economic return # Suppose implementing any feasible action generates 10 monetary units, # regardless of the action or its location. A scalar is applied to all # feasible planning-unit--action pairs. p_constant <- add_profit(p, profit = 10) head(p_constant$data$dist_profit[, c("pu", "action", "profit")]) # EXAMPLE 3: Action-specific returns # Protection generates +50 monetary units, whereas restoration incurs # an economic penalty of -5 units. A named vector assigns a value to # every feasible planning unit where that action is available. returns <- c(protect = 50, restore = -5) p_action <- add_profit(p, profit = returns) head(p_action$data$dist_profit[, c("pu", "action", "profit")]) # The same action-level values can be supplied as a data frame. # These alternatives start from the same base problem because profit # can be defined only once per problem. p_action_table <- add_profit(p, data.frame( action = c("protect", "restore"), profit = c(50, -5) )) head(p_action_table$data$dist_profit[, c("pu", "action", "profit")]) # EXAMPLE 4: Location-specific economic returns # The same action can have different returns across planning units. # Protection provides +100 in unit 1 and +80 in unit 2; restoration # provides +30 in unit 3. Unlisted feasible pairs receive zero profit. local_returns <- data.frame( pu = c(1, 2, 3), action = c("protect", "protect", "restore"), profit = c(100, 80, 30) ) p_local <- add_profit(p, profit = local_returns) # Only the three non-zero contributions are stored. This does not # make any of the other, zero-profit action pairs infeasible. p_local$data$dist_profit[, c("pu", "action", "profit")] # EXAMPLE 5: Explicitly specify no economic return # NULL assigns zero to every feasible pair. The resulting sparse # profit table is empty, even though the actions remain available. p_zero <- add_profit(p, profit = NULL) nrow(p_zero$data$dist_profit) # EXAMPLE 6: Map spatially varying potential returns # The simulated planning units include geometry and an x-coordinate. # Give restoration a hypothetical return that increases from west to # east. These are potential returns, not selected management actions. # No optimiser or solver is needed to visualise the input values. if (requireNamespace("sf", quietly = TRUE)) { spatial_returns <- data.frame( pu = sim$planning_units$id, action = "restore", profit = 20 + 5 * sim$planning_units$x ) p_spatial <- add_profit(p, profit = spatial_returns) # Attach the registered restoration returns to the geometry. values <- subset( p_spatial$data$dist_profit, action == "restore", select = c("pu", "profit") ) mapped <- merge( sim$planning_units, values, by.x = "id", by.y = "pu", all.x = TRUE ) # This map describes where restoration could generate economic # returns, not where optimisation has selected restoration. plot(mapped["profit"], main = "Potential economic return from restoration") }
Build and register a boundary-length spatial relation between planning units.
Boundary relations represent shared edge length between adjacent polygons. In contrast to queen adjacency, they only account for boundary segments of positive length and ignore point-only contacts.
add_spatial_boundary( x, boundary = NULL, geometry = NULL, name = "boundary", weight_col = NULL, weight_multiplier = 1, include_self = TRUE, edge_factor = 1 )add_spatial_boundary( x, boundary = NULL, geometry = NULL, name = "boundary", weight_col = NULL, weight_multiplier = 1, include_self = TRUE, edge_factor = 1 )
x |
A |
boundary |
Optional
|
geometry |
Optional |
name |
Character string giving the key under which the relation is stored. |
weight_col |
Optional character string giving the name of the weight
column in |
weight_multiplier |
Positive numeric scalar applied to all boundary weights. |
include_self |
Logical. If |
edge_factor |
Numeric scalar greater than or equal to zero. Multiplier applied to exposed boundary when constructing diagonal entries. |
Use this function when spatial structure should be represented through shared boundary length rather than binary contiguity or coordinate-based proximity.
Two input modes are supported:
Boundary-table mode. If boundary is supplied, it is
interpreted as a boundary table, for example a Marxan-style
bound.dat.
Geometry mode. If boundary = NULL, boundary lengths
are derived from polygon geometry using geometry or
x$data$pu_sf.
Let denote the shared boundary length between
planning units and , multiplied by
weight_multiplier.
For off-diagonal entries , the stored weight is:
where is the shared boundary length and is
the user-supplied weight_multiplier.
If include_self = TRUE, diagonal entries are also created. These are
not geometric self-neighbours in the graph sense; instead, they represent the
effective boundary exposed to the outside of the solution.
Let be the total perimeter of planning unit , and let
be the total incident shared boundary
recorded for that planning unit. Then the exposed boundary is represented by
a diagonal term derived from the difference between total perimeter and shared
boundary, scaled by edge_factor.
These diagonal terms are useful in boundary-based compactness or fragmentation objectives, because they encode the portion of each planning unit's perimeter that would remain exposed if the unit were selected.
Boundary-table mode
If boundary is provided, accepted formats are:
(id1, id2, boundary), or
(pu1, pu2, weight).
If the table contains diagonal rows , these are interpreted as
total perimeter values in boundary-table mode.
Geometry mode
If boundary = NULL, shared boundary lengths are derived directly from
polygon geometry. Only positive-length intersections are retained. Point
touches are ignored.
Storage
The final relation is stored through add_spatial_relations,
typically as an undirected relation with optional diagonal entries.
An updated Problem object with the stored relation in
x$data$spatial_relations[[name]].
add_spatial_relations,
add_objective_min_fragmentation_pu,
add_objective_min_fragmentation_action
# 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" ) p <- add_spatial_boundary( x = p, name = "boundary", include_self = TRUE, edge_factor = 1 ) p$data$spatial_relations$boundary# 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" ) p <- add_spatial_boundary( x = p, name = "boundary", include_self = TRUE, edge_factor = 1 ) p$data$spatial_relations$boundary
Build and register a spatial relation connecting planning units whose Euclidean distance is less than or equal to a user-defined threshold.
This constructor does not require polygon geometry and instead uses planning-unit coordinates.
add_spatial_distance( x, coords = NULL, max_distance, name = "distance", weight_mode = c("constant", "inverse", "inverse_sq"), distance_eps = 1e-09 )add_spatial_distance( x, coords = NULL, max_distance, name = "distance", weight_mode = c("constant", "inverse", "inverse_sq"), distance_eps = 1e-09 )
x |
A |
coords |
Optional coordinates specification, following the same rules as
in |
max_distance |
Positive numeric scalar giving the maximum distance for an edge. |
name |
Character string giving the key under which the relation is stored. |
weight_mode |
Character string indicating how distance is converted to
weight. Must be one of |
distance_eps |
Small positive numeric constant used to avoid division by zero in inverse-distance weighting. |
Use this function when neighbourhood should be defined by a fixed distance radius rather than by polygon topology or a fixed number of neighbours.
Let denote the coordinates of planning unit
. Let be the Euclidean distance between planning units
and .
For a user-supplied threshold , this constructor creates an
edge between and whenever:
Edge weights are assigned according to weight_mode:
"constant":
"inverse":
"inverse_sq":
where = distance_eps is a small constant.
The implementation computes an distance matrix and is therefore
best suited to small or moderate numbers of planning units. For large
problems, add_spatial_knn is often more scalable.
The resulting relation is registered as undirected.
An updated Problem object.
add_spatial_knn,
add_spatial_relations
# 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" ) p <- add_spatial_distance( x = p, max_distance = 1.01, name = "within_1", weight_mode = "constant" ) p$data$spatial_relations$within_1# 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" ) p <- add_spatial_distance( x = p, max_distance = 1.01, name = "within_1", weight_mode = "constant" ) p$data$spatial_relations$within_1
Build and register a k-nearest-neighbours graph between planning units using coordinates.
This constructor does not require polygon geometry. It uses planning-unit
coordinates supplied explicitly or stored in the Problem object.
add_spatial_knn( x, coords = NULL, k = 8, name = "knn", weight_mode = c("constant", "inverse", "inverse_sq"), distance_eps = 1e-09 )add_spatial_knn( x, coords = NULL, k = 8, name = "knn", weight_mode = c("constant", "inverse", "inverse_sq"), distance_eps = 1e-09 )
x |
A |
coords |
Optional coordinates specification. This may be:
If |
k |
Integer giving the number of neighbours per planning unit. Must be at least 1 and strictly less than the number of planning units. |
name |
Character string giving the key under which the relation is stored. |
weight_mode |
Character string indicating how distance is converted to
weight. Must be one of |
distance_eps |
Small positive numeric constant used to avoid division by zero in inverse-distance weighting. |
Use this function when neighbourhood should be defined by a fixed number of nearby planning units rather than by polygon topology or a fixed distance threshold.
Let denote the coordinates of planning unit
. For each planning unit, this function identifies the k
nearest distinct planning units under Euclidean distance.
If denotes the Euclidean distance between units and
, then the k-nearest-neighbours relation is constructed by adding an
edge from to each of its k nearest neighbours.
Edge weights are then assigned according to weight_mode:
"constant":
"inverse":
"inverse_sq":
where = distance_eps is a small constant to avoid
division by zero.
The raw k-nearest-neighbours structure is directional by construction, but
the stored relation is registered as undirected by default through
add_spatial_relations, which collapses duplicate unordered
pairs.
If the RANN package is available, it is used for efficient nearest neighbour search. Otherwise, a full distance matrix is computed.
An updated Problem object.
add_spatial_distance,
add_spatial_relations
# 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" ) p <- add_spatial_knn( x = p, k = 2, name = "knn2", weight_mode = "constant" ) p$data$spatial_relations$knn2# 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" ) p <- add_spatial_knn( x = p, k = 2, name = "knn2", weight_mode = "constant" ) p$data$spatial_relations$knn2
Build and register a queen adjacency relation from planning-unit polygons.
Two planning units are queen-adjacent if their boundaries touch, either along a shared edge or at a shared vertex.
add_spatial_queen(x, geometry = NULL, name = "queen", weight = 1)add_spatial_queen(x, geometry = NULL, name = "queen", weight = 1)
x |
A |
geometry |
Optional |
name |
Character string giving the key under which the relation is stored. |
weight |
Numeric scalar giving the edge weight assigned to each queen adjacency. |
Use this function when neighbourhood should include both shared edges and corner-touching polygon contacts.
This constructor derives an adjacency graph from polygon geometry using a
queen criterion. If planning units and touch at any boundary
point, then an edge is added to the relation.
Let denote the resulting graph. Then:
Thus, queen adjacency includes all rook neighbours plus corner-touching neighbours.
All edges receive the same user-supplied weight.
The resulting relation is stored as an undirected spatial relation.
An updated Problem object.
add_spatial_rook,
add_spatial_boundary
# 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" ) p <- add_spatial_queen( x = p, geometry = example_data$planning_units, name = "queen", weight = 1 ) head(p$data$spatial_relations$queen)# 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" ) p <- add_spatial_queen( x = p, geometry = example_data$planning_units, name = "queen", weight = 1 ) head(p$data$spatial_relations$queen)
Register an externally computed spatial relation inside a Problem
object using the unified internal representation adopted by multiscape.
Most users will typically prefer one of the convenience constructors such as
add_spatial_boundary, add_spatial_rook,
add_spatial_queen, add_spatial_knn, or
add_spatial_distance. This function is the advanced low-level
entry point for adding an already computed relation.
add_spatial_relations(x, relations, name, directed = FALSE, allow_self = FALSE)add_spatial_relations(x, relations, name, directed = FALSE, allow_self = FALSE)
x |
A |
relations |
A
Extra columns such as |
name |
Character string giving the key under which the relation is stored. |
directed |
Logical. If |
allow_self |
Logical. If |
Use this function when the spatial relation has already been computed
externally and should be registered directly in the Problem object.
The input relation may be provided either in terms of external planning-unit identifiers or in terms of internal planning-unit indices.
Specifically, the input relations table must contain either:
pu1, pu2, and weight, or
internal_pu1, internal_pu2, and weight.
If external ids are supplied, they are mapped to internal indices using the planning-unit identifiers stored in the problem.
Let denote the supplied relation, where
corresponds to the rows of relations. If
directed = FALSE, each edge is treated as undirected, so pairs
and are interpreted as the same edge. In that case,
each unordered pair must be supplied exactly once. Duplicate undirected
edges are rejected, including reciprocal rows.
If directed = TRUE, edges are preserved as ordered pairs, so
and are distinct unless the user provides both.
#' Self-edges are permitted only if
allow_self = TRUE. In fragmentation objectives, diagonal entries
are interpreted as unary planning-unit terms ,
including for directed relations; they are not interpreted as directed
self-dependencies.
The final relation is stored in x$data$spatial_relations[[name]].
Calls accumulate relations with distinct names. If a relation with the same
name already exists, an error is raised, including through spatial
constructors. To change it, rebuild from the problem before it was added.
An updated Problem object with the relation stored in
x$data$spatial_relations[[name]].
add_spatial_boundary,
add_spatial_rook,
add_spatial_queen,
add_spatial_knn,
add_spatial_distance
# 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" ) rel <- data.frame( pu1 = c(1, 1, 2), pu2 = c(2, 3, 3), weight = c(1, 1, 2) ) p <- add_spatial_relations( x = p, relations = rel, name = "my_relation" ) p$data$spatial_relations$my_relation# 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" ) rel <- data.frame( pu1 = c(1, 1, 2), pu2 = c(2, 3, 3), weight = c(1, 1, 2) ) p <- add_spatial_relations( x = p, relations = rel, name = "my_relation" ) p$data$spatial_relations$my_relation
Build and register a rook adjacency relation from planning-unit polygons.
Two planning units are rook-adjacent if they share a boundary segment of positive length. Corner-only contact does not count as rook adjacency.
add_spatial_rook(x, geometry = NULL, name = "rook", weight = 1)add_spatial_rook(x, geometry = NULL, name = "rook", weight = 1)
x |
A |
geometry |
Optional |
name |
Character string giving the key under which the relation is stored. |
weight |
Numeric scalar giving the edge weight assigned to each rook adjacency. |
Use this function when neighbourhood should be defined by shared polygon edges rather than by point-touching or coordinate-based proximity.
This constructor derives an adjacency graph from polygon geometry using a
rook criterion. If planning units and share a common edge of
non-zero length, then an edge is added to the relation.
Let denote the resulting graph. Then:
All edges receive the same user-supplied weight.
The resulting relation is stored as an undirected spatial relation.
An updated Problem object.
add_spatial_queen,
add_spatial_boundary
# 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" ) p <- add_spatial_rook( x = p, geometry = example_data$planning_units, name = "rook", weight = 1 ) head(p$data$spatial_relations$rook)# 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" ) p <- add_spatial_rook( x = p, geometry = example_data$planning_units, name = "rook", weight = 1 ) head(p$data$spatial_relations$rook)
Materializes the optimization model represented by a Problem object
without solving it. This is an advanced function mainly intended for
debugging, inspection, and explicit model preparation.
In standard workflows, users normally do not need to call this function,
because solve compiles the model automatically when needed.
compile_model(x, force = FALSE, ...) ## S3 method for class 'Problem' compile_model(x, force = FALSE, ...)compile_model(x, force = FALSE, ...) ## S3 method for class 'Problem' compile_model(x, force = FALSE, ...)
x |
A |
force |
Logical. If |
... |
Reserved for future extensions. |
Use this function when you want to prepare the optimization model explicitly before solving, inspect compiled model structures, or verify that the problem compiles successfully.
Conceptually, a Problem object stores a declarative optimization
specification: planning data, actions, effects, targets, constraints,
objectives, spatial relations, and optional method or solver settings.
compile_model() transforms that stored specification into an internal
compiled model representation that can later be reused by the solving layer.
The exact compiled representation is implementation-specific, but it may include indexed variables, prepared constraint blocks, objective structures, and internal model snapshots or pointers.
Compilation does not solve the optimization problem. Therefore, a problem may compile successfully and still later be infeasible, numerically difficult, or otherwise fail during solver execution.
A Problem object with compiled model structures stored internally.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.3) |> add_objective_min_cost(alias = "cost") x <- compile_model(x) # Force recompilation x <- compile_model(x, force = TRUE)# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.3) |> add_objective_min_cost(alias = "cost") x <- compile_model(x) # Force recompilation x <- compile_model(x, force = TRUE)
Create a Problem object from tabular or spatial inputs.
create_problem() is the main entry point to the multiscape
workflow. Its role is to standardize heterogeneous planning inputs into a
common internal representation that can later be used by actions, effects,
targets, constraints, spatial relations, objectives, and solvers.
In all supported workflows, the result is a Problem object with a
canonical tabular core, optionally enriched with aligned spatial metadata
such as coordinates, geometry, raster references, or raw planning-unit
attributes.
create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'data.frame,data.frame,data.frame' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,ANY,missing' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,ANY,NULL' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,data.frame,data.frame' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... )create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'data.frame,data.frame,data.frame' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,ANY,missing' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,ANY,NULL' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... ) ## S4 method for signature 'ANY,data.frame,data.frame' create_problem( pu, features, dist_features, cost = NULL, pu_id_col = "id", cost_aggregation = c("mean", "sum"), ... )
pu |
Planning-units input. Depending on the selected method, this may be:
|
features |
Feature input. Depending on the selected workflow, this may be:
|
dist_features |
Feature-distribution input. In tabular and hybrid
workflows, this must be a |
cost |
In spatial modes, planning-unit cost information. Depending on the input mode, this may be:
In raster-cell mode, |
pu_id_col |
Character string giving the name of the planning-unit id
column in vector or |
cost_aggregation |
Character string used in vector-PU mode when
|
... |
Additional arguments forwarded to internal builders. |
Feature amounts describe a user-defined reference scenario, which can be current conditions, a future business-as-usual scenario, or existing management. The term baseline in older interfaces refers to these reference amounts. Action outcomes and reference amounts must share units and, for future scenarios, the same time horizon. The package optimizes supplied effects; it does not estimate causal effects or simulate reference scenarios.
A Problem object created by create_problem() is the basic input
structure used throughout downstream multiscape workflows.
The returned object always contains a canonical tabular planning core and may
additionally store aligned spatial metadata depending on the input workflow.
This is the internal representation on which later functions such as
add_actions(), add_effects(),
add_constraint_targets(), add_spatial_relations(), and
solve() operate.
Conceptual role of create_problem()
Regardless of the input workflow, create_problem() aims to produce a
consistent internal representation containing at least:
a planning-unit table,
a feature table,
a feature-distribution table.
Internally, the problem is always reduced to a tabular core of the form:
planning units ,
features ,
non-negative baseline amounts ,
and, when available, spatial metadata associated with planning units.
The feature-distribution table provides the canonical sparse representation of
these baseline amounts. Thus, after create_problem() has run, the
landscape is internally represented by baseline feature amounts
, where denotes the amount of feature in
planning unit . These baseline amounts are later combined with
actions, effects, targets, objectives, and constraints to build the
optimization model.
Which input mode should I use?
create_problem() supports four main workflows. The best choice depends
on the form of your data and on whether you want to preserve geometry.
Tabular mode. Use this when pu, features, and
dist_features are already available as data.frame objects and
no spatial derivation is needed. This is the simplest workflow: all problem
components are already tabular, so create_problem() only
standardizes them into the canonical internal representation.
Vector-PU spatial mode. Use this when planning units are
polygons and feature information is stored in one or more raster layers. In
this mode, dist_features is derived by aggregating raster values
over planning-unit polygons.
Raster-cell fast mode. Use this when both pu and
features are rasters and each valid raster cell should become one
planning unit. This mode avoids raster-to-polygon conversion, treats
NA feature values as zero before building dist_features,
keeps only strictly positive amounts in the stored distribution table, and
is generally the preferred option for large regular grids.
Hybrid sf + tabular mode. Use this when you already have a
curated tabular dist_features table but still want to preserve
planning-unit geometry and attributes for later plotting, spatial
relations, or feasibility specifications. Here, the feature distribution is
already tabular, but geometry and raw attributes are preserved from the
sf planning-unit object for later spatial operations.
How cost is interpreted across modes
cost handling depends on the selected workflow.
Tabular mode. In purely tabular mode, cost is not
used by this generic method. Planning-unit costs are expected to already be
present in the pu table supplied to the internal tabular builder.
Vector-PU spatial mode. In vector-PU mode, cost is
required and may be either:
the name of a numeric attribute column in the planning-unit layer,
or a cost raster to be aggregated over polygons using the
cost_aggregation argument.
Raster-cell fast mode. In raster-cell mode, cost
must be a single-layer raster aligned with pu and features.
A raster cell becomes a planning unit only if the mask cell is not missing
and the corresponding cost value is finite and strictly positive. In other
words, if denotes the mask value of cell and
its cost value, then cell is retained only when the mask is
observed and .
Hybrid sf + tabular mode. In hybrid mode, cost must
be either:
the name of a numeric attribute column in the sf layer,
or omitted if the sf attributes already contain a column
literally named cost.
Feature identifiers
Features are internally standardized to an id-based representation.
In spatial modes where raster layers are used, one feature is created per
raster layer, with:
id = 1, 2, ..., nlyr(features),
name equal to the raster layer name, if available,
otherwise a generated name of the form "feature.1",
"feature.2", and so on.
Planning-unit identifiers
In vector and hybrid modes, planning-unit ids are taken from the column named
by pu_id_col. If that column is missing and pu_id_col = "id",
sequential ids are created with a warning.
In raster-cell mode, planning-unit ids are always created sequentially from the valid raster cells retained after masking and cost filtering.
Ordering and alignment
In spatial modes, planning units, derived coordinates, raw attributes,
geometry, and extracted feature amounts are aligned to the same planning-unit
id order before the final Problem object is created.
This alignment is critical because later functions assume that all stored planning-unit components refer to the same ordered set of planning units.
After create_problem(), typical next steps include adding actions,
spatial relations, targets or other constraints, objectives, and then solving
the resulting problem.
A Problem object for downstream multiscape workflows.
The returned object always contains a canonical tabular core with planning
units, features, and feature-distribution data, and may additionally
contain aligned spatial metadata depending on the input mode.
add_actions,
add_constraint_locked_pu,
add_spatial_boundary,
add_spatial_knn,
solve
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() # Spatial mode: keep planning-unit geometries. p1 <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) print(p1) # Tabular mode: use the same data without geometry. p2 <- create_problem( pu = sf::st_drop_geometry(example_data$planning_units), features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) print(p2)# Load a complete simulated planning problem. example_data <- load_sim_multiaction() # Spatial mode: keep planning-unit geometries. p1 <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) print(p1) # Tabular mode: use the same data without geometry. p2 <- create_problem( pu = sf::st_drop_geometry(example_data$planning_units), features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) print(p2)
Compute normalized distances from each stored solution in a
solutionset-class object to the observed ideal and/or nadir
point in objective space.
frontier_distances( x, objectives = NULL, reference = "ideal", metric = c("euclidean", "manhattan", "chebyshev") )frontier_distances( x, objectives = NULL, reference = "ideal", metric = c("euclidean", "manhattan", "chebyshev") )
x |
A |
objectives |
Optional character vector of objective names to use. If
|
reference |
Character vector indicating the reference point or points
to use. Allowed values are |
metric |
Character. Distance metric to use. One of
|
This function supports the interpretation and ranking of trade-offs among
solutions stored in a SolutionSet.
The calculations are based on the solutions contained in the supplied
object. Therefore, the observed ideal point, observed nadir point, objective
ranges, normalized values, and distances may change when the input
SolutionSet is filtered.
To calculate distances using only non-dominated solutions, first use:
x_nd <- solution_filter(x, nondominated = TRUE) frontier_distances(x_nd)
Objective values are internally transformed to a common minimization space
using the objective senses registered in the original problem.
sense = "min" are kept unchanged, whereas objectives with
sense = "max" are multiplied by . In this transformed space,
lower values are always better.
The observed ideal point is defined by the best observed value for each objective:
where is the transformed value of solution for
objective .
The observed nadir point is defined by the worst observed value for each objective:
These reference points are empirical bounds derived from the supplied solutions. They are not necessarily the true ideal and nadir points of the complete feasible objective space.
Objective values are normalized to the interval using:
After normalization:
0 represents the best observed value for an objective;
1 represents the worst observed value for an objective.
This interpretation is independent of whether the original objective was minimized or maximized.
If an objective has zero observed range, all normalized values for that objective are set to zero. The objective therefore does not contribute to the calculated distances.
For distances to the ideal point, smaller values are preferred and
rank_to_ideal = 1 identifies the closest solution.
For distances to the nadir point, larger values are preferred and
rank_from_nadir = 1 identifies the solution farthest from the
observed nadir point.
A data.frame with one row per stored solution having complete
values for the selected objectives.
The table contains:
solution_id: integer solution id;
the original objective values;
normalized objective values prefixed with norm_;
distance and rank columns for the requested reference points.
The returned table also contains the following attributes:
"ideal": observed ideal point in the original objective
scales;
"nadir": observed nadir point in the original objective
scales;
"ranges": observed absolute ranges in the original objective
scales;
"objectives": objective names used;
"sense": optimization sense of each objective;
"metric": distance metric used;
"reference": requested reference point or points;
"normalized": whether normalized values were used;
"space": objective space used for distance calculations.
frontier_extremes,
get_objectives,
solution_filter
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Normalized Euclidean distance to the observed ideal point frontier_distances(solutions) # Distances to both observed ideal and nadir points distances <- frontier_distances( solutions, reference = c("ideal", "nadir") ) # Use only selected objectives frontier_distances( solutions, objectives = c("cost", "benefit") ) # Use Manhattan distance frontier_distances( solutions, metric = "manhattan" ) # Use Chebyshev distance frontier_distances( solutions, metric = "chebyshev" ) # Inspect observed reference points and objective ranges attr(distances, "ideal") attr(distances, "nadir") attr(distances, "ranges") # Calculate distances only over non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) frontier_distances( nondominated_solutions, reference = c("ideal", "nadir") ) } }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Normalized Euclidean distance to the observed ideal point frontier_distances(solutions) # Distances to both observed ideal and nadir points distances <- frontier_distances( solutions, reference = c("ideal", "nadir") ) # Use only selected objectives frontier_distances( solutions, objectives = c("cost", "benefit") ) # Use Manhattan distance frontier_distances( solutions, metric = "manhattan" ) # Use Chebyshev distance frontier_distances( solutions, metric = "chebyshev" ) # Inspect observed reference points and objective ranges attr(distances, "ideal") attr(distances, "nadir") attr(distances, "ranges") # Calculate distances only over non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) frontier_distances( nondominated_solutions, reference = c("ideal", "nadir") ) } }
Identify the observed minimum and maximum values for each objective in a
solutionset-class object.
This function returns the solutions that define the observed range of each
selected objective. It also labels each extreme as "best" or
"worst" according to the registered optimization sense of the
objective.
frontier_extremes(x, objectives = NULL, ties = c("all", "first"))frontier_extremes(x, objectives = NULL, ties = c("all", "first"))
x |
A |
objectives |
Optional character vector of objective names to inspect.
If |
ties |
Character. How to handle ties. If |
Objective values are obtained from the stored run table. Objective senses are obtained from the objective specifications stored in the original problem.
For objectives with sense = "min", the observed minimum is labelled
as "best" and the observed maximum is labelled as "worst".
For objectives with sense = "max", the observed maximum is labelled
as "best" and the observed minimum is labelled as "worst".
Runs without a stored solution_id or with missing objective values for
the selected objectives are ignored automatically. Therefore, infeasible runs
are not considered in the computation.
If several solutions have the same extreme value for an objective, the
behaviour is controlled by ties.
A data.frame with one or more rows per objective. The returned
columns are:
solution_id: integer solution id;
objective: objective name;
sense: optimization sense, either "min" or
"max";
bound: observed bound, either "min" or "max";
role: interpretation of the bound, either "best" or
"worst";
value: objective value at the observed bound.
get_objectives,
solution_filter
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Observed minimum and maximum for every objective frontier_extremes(solutions) # Inspect only selected objectives frontier_extremes( solutions, objectives = c("cost", "benefit") ) # Keep only the first solution when several solutions share an extreme frontier_extremes( solutions, ties = "first" ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Observed minimum and maximum for every objective frontier_extremes(solutions) # Inspect only selected objectives frontier_extremes( solutions, objectives = c("cost", "benefit") ) # Keep only the first solution when several solutions share an extreme frontier_extremes( solutions, ties = "first" ) }
Identify empirical knee, or compromise, solutions from the objective values
stored in a solutionset-class object.
frontier_knee( x, objectives = NULL, method = c("distance", "ideal", "angle"), metric = c("euclidean", "manhattan", "chebyshev"), nondominated = TRUE, ties = c("first", "all"), return_all = FALSE )frontier_knee( x, objectives = NULL, method = c("distance", "ideal", "angle"), metric = c("euclidean", "manhattan", "chebyshev"), nondominated = TRUE, ties = c("first", "all"), return_all = FALSE )
x |
A |
objectives |
Optional character vector of objective names to use. If
|
method |
Character. Method used to score knee solutions. One of
|
metric |
Character. Distance metric used when |
nondominated |
Logical. If |
ties |
Character. How to handle ties when |
return_all |
Logical. If |
A knee solution is a solution located in a region of the observed frontier
where small improvements in one objective tend to require relatively large
losses in another objective. Because this concept can be defined in several
ways, frontier_knee() provides multiple scoring methods.
Objective values are first transformed to a common minimization space using
the optimization sense registered in the original problem. Objectives with
sense = "min" are kept unchanged, whereas objectives with
sense = "max" are multiplied by . Values are then normalized
to the observed range, where 0 represents the best
observed value and 1 represents the worst observed value for each
objective.
The available methods are:
"distance": identifies the solution with the largest
perpendicular distance to the line connecting the two observed
objective-wise extreme solutions. This method requires exactly two
objectives and is the default geometric knee definition.
"ideal": identifies the solution closest to the observed
ideal point in normalized objective space. This method can be used with
two or more objectives and is best interpreted as a compromise solution.
"angle": identifies the solution with the largest change in
direction along the observed bi-objective frontier. This method requires
exactly two objectives and at least three complete solutions.
By default, frontier_knee() first filters the supplied
SolutionSet to feasible non-dominated solutions using
solution_filter. Set nondominated = FALSE to compute
the knee over all stored solutions with complete objective values.
The returned knee is empirical and depends on the supplied
SolutionSet. It should not be interpreted as the unique knee of the
full feasible objective space unless the supplied solutions adequately
represent the frontier.
A data.frame. If return_all = FALSE, the table contains
the selected knee solution or solutions. If return_all = TRUE, the
table contains all candidate solutions ranked by knee score.
The returned table includes:
solution_id: integer solution id;
the original objective values;
normalized objective values prefixed with norm_;
method-specific diagnostic columns;
knee_score: score used to rank knee solutions, where larger
values indicate stronger knee candidates;
knee_rank: rank of each solution according to
knee_score;
method: knee scoring method used.
The returned table also contains the following attributes:
"ideal": observed ideal point in the original objective
scales;
"nadir": observed nadir point in the original objective
scales;
"ranges": observed absolute ranges in the original objective
scales;
"objectives": objective names used;
"sense": optimization sense of each objective;
"method": knee method used;
"nondominated": whether non-dominated filtering was applied;
"space": objective space used for scoring.
frontier_distances,
frontier_extremes,
get_objectives,
solution_filter
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Default geometric knee frontier_knee(solutions) # Return all solutions ranked by knee score frontier_knee( solutions, return_all = TRUE ) # Closest solution to the observed ideal point frontier_knee( solutions, method = "ideal" ) # Largest change in direction along the bi-objective frontier frontier_knee( solutions, method = "angle" ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Default geometric knee frontier_knee(solutions) # Return all solutions ranked by knee score frontier_knee( solutions, return_all = TRUE ) # Closest solution to the observed ideal point frontier_knee( solutions, method = "ideal" ) # Largest change in direction along the bi-objective frontier frontier_knee( solutions, method = "angle" ) }
Connect nearby stored solutions in normalized objective space. The resulting pairs can be supplied directly to objective–decision linkage functions.
frontier_neighbors( x, objectives = NULL, method = c("auto", "sequence", "mst", "knn"), metric = c("euclidean", "manhattan", "chebyshev"), k = 1L )frontier_neighbors( x, objectives = NULL, method = c("auto", "sequence", "mst", "knn"), metric = c("euclidean", "manhattan", "chebyshev"), k = 1L )
x |
A |
objectives |
Optional character vector of objective aliases. If
|
method |
Neighborhood method: |
metric |
Objective-space distance metric: |
k |
Number of nearest neighbors used only by The first selected objective orients all pairs and, for
|
Objective values are oriented to minimization and normalized using the
solutions retained in the supplied SolutionSet. The function does not
remove dominated or repeated solutions; use solution_filter or
solution_unique beforehand when required.
method = "auto" uses a sequence for one or two objectives and a
minimum spanning tree for higher-dimensional objective spaces.
A data.frame with from_solution,
to_solution, objective_distance, and objective-specific
values and changes. Analysis settings are retained as attributes.
frontier_distances, linkage_distances,
linkage_turnover
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("benefit", "cost") ) neighbors frontier_neighbors( solutions, objectives = c("cost", "benefit"), method = "knn", k = 2 ) }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("benefit", "cost") ) neighbors frontier_neighbors( solutions, objectives = c("cost", "benefit"), method = "knn", k = 2 ) }
Return the definitions registered by add_action_sets() in long format.
This function inspects a planning problem, not selected actions in a solution.
get_action_sets(x)get_action_sets(x)
x |
A |
A data.frame with character columns set and action, sorted by
set and action id. If no sets are registered, a zero-row table with the
same columns is returned. Internal identifiers are not exposed.
add_action_sets(), get_actions()
problem <- create_problem( pu = data.frame(id = 1:2, cost = 1), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 10) ) |> add_actions(data.frame(id = c("restore", "control"))) get_action_sets(problem) problem <- add_action_sets( problem, list(restore_control = c("restore", "control")) ) get_action_sets(problem)problem <- create_problem( pu = data.frame(id = 1:2, cost = 1), features = data.frame(id = 1L, name = "woodland"), dist_features = data.frame(pu = 1:2, feature = 1L, amount = 10) ) |> add_actions(data.frame(id = c("restore", "control"))) get_action_sets(problem) problem <- add_action_sets( problem, list(restore_control = c("restore", "control")) ) get_action_sets(problem)
Extract the action-allocation summary table from a
solutionset-class object returned by solve.
The returned table summarizes solution values at the
planning unit–action level and typically includes a selected
indicator showing whether each feasible (pu, action) pair is selected
in a solution.
get_actions(x, solution = NULL, ...)get_actions(x, solution = NULL, ...)
x |
A |
solution |
Optional positive integer giving the solution id to extract. If
|
... |
Deprecated arguments kept for backwards compatibility. Currently
supports |
This function reads the action summary stored in x$summary$actions. It
does not reconstruct the table from the raw decision vector; it simply
returns the stored summary after optional run filtering.
Let denote the decision variable associated with selecting
action in planning unit . In standard multiscape
workflows, the selected column is the user-facing representation of
that decision, typically coded as 0 or 1.
If solution is provided, only rows belonging to that solution are returned. This
requires the summary table to contain a solution_id column.
To return only selected action allocations, filter the returned table using
selected == 1.
A data.frame containing the stored action-allocation summary.
Typical columns include planning-unit ids, action ids, optional labels, and
a selected indicator.
get_planning_units,
get_features,
get_targets
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # All feasible planning-unit/action assignments get_actions(solutions) # Only selected action assignments selected_actions <- get_actions(solutions) selected_actions <- selected_actions[ selected_actions$selected == 1L, , drop = FALSE ] selected_actions # Action allocations for one solution solution_ids <- get_runs(solutions)$solution_id get_actions( solutions, solution = solution_ids[1] ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # All feasible planning-unit/action assignments get_actions(solutions) # Only selected action assignments selected_actions <- get_actions(solutions) selected_actions <- selected_actions[ selected_actions$selected == 1L, , drop = FALSE ] selected_actions # Action allocations for one solution solution_ids <- get_runs(solutions)$solution_id get_actions( solutions, solution = solution_ids[1] ) }
Extract the per-feature summary table from a
solutionset-class object returned by solve.
The returned table summarizes, for each feature, how much of the feature was available in the full baseline landscape and how much is represented by the selected planning units or selected actions in each run.
get_features(x, solution = NULL, ...)get_features(x, solution = NULL, ...)
x |
A |
solution |
Optional positive integer giving the solution id to extract. If
|
... |
Deprecated arguments kept for backwards compatibility. Currently
supports |
This function reads the feature summary stored in x$summary$features.
It errors if that table is missing.
Feature summaries distinguish between baseline availability in the full landscape and the contribution of selected planning units or selected actions.
Let denote the total baseline amount of feature available
in the full landscape. Let denote the baseline amount of feature
in selected rows. Let denote the after-action amount of
feature contributed by those selected rows. Let and
denote the positive and negative net-change components induced by
selected actions. Then:
and:
The main returned columns are:
baseline_total: total baseline amount in the full landscape;
selected_baseline: baseline amount in selected rows;
selected_amount_after: after-action amount contributed by
selected rows;
selected_benefit: positive net-change component from selected
actions;
selected_loss: negative net-change component from selected
actions;
selected_net: net change from selected actions;
selected_fraction_of_baseline: ratio between
selected_amount_after and baseline_total.
Importantly, this summary does not assume that planning units without a
selected action contribute to the achieved feature amount. Therefore, the
achieved amount for a feature is represented by
selected_amount_after, not by a full-landscape total obtained by adding
net changes to the baseline.
For backwards compatibility with older result objects, if the newer
selected-action columns are missing, this function attempts to construct them
from older columns such as total_available, benefit,
loss, net, and amount_after. However, the returned table
is organized using the newer selected-action terminology.
If solution is provided, only rows belonging to that solution are returned. If
the result contains a solution_id column but only a single solution is present and
solution was not requested explicitly, the solution_id column is removed
for convenience.
Positive gains and losses are split after combining actions within each unit
and feature. With matching action/feature scopes, the benefit objective
corresponds to the sum of selected_net, and the loss objective to the
sum of selected_loss. selected_benefit remains a descriptive
positive-gain component and is not the signed benefit objective.
This function summarizes feature outcomes in the result. It is different from
get_targets, which focuses on target achievement.
A data.frame with one row per feature, or one row per
feature–run combination when multiple runs are present. The returned table
includes, when available or derivable, the columns
baseline_total, selected_baseline,
selected_amount_after, selected_benefit,
selected_loss, selected_net, and
selected_fraction_of_baseline.
get_planning_units,
get_actions,
get_targets
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Feature outcomes for all stored runs get_features(solutions) # Feature outcomes for one run solution_ids <- get_runs(solutions)$solution_id get_features( solutions, solution = solution_ids[1] ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Feature outcomes for all stored runs get_features(solutions) # Feature outcomes for one run solution_ids <- get_runs(solutions)$solution_id get_features( solutions, solution = solution_ids[1] ) }
Extract objective values from the runs stored in a
solutionset-class object.
get_objectives(x, format = c("wide", "long"))get_objectives(x, format = c("wide", "long"))
x |
A |
format |
Character. Output representation, either |
Objective values are read from run-table columns named
value_<objective>, where <objective> is the registered
objective alias.
Runs without a stored solution may contain missing objective values. Filter
the SolutionSet beforehand with solution_filter when
only solved runs should be included.
Public objective tables are keyed by integer solution_id. Use
get_runs when the relationship between attempted
run_ids and stored solutions is required. In long format, every
solution-objective combination occupies one row. In wide format, every
stored solution occupies one row and every objective occupies one column.
If format = "long", a data.frame with columns
solution_id, objective, and value.
If format = "wide", a data.frame with integer
solution_id and one column per objective.
get_runs,
frontier_extremes,
frontier_distances
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Long format get_objectives( solutions, format = "long" ) # Wide format get_objectives( solutions, format = "wide" ) # Objective values from usable runs only usable_solutions <- solution_filter( solutions, feasible_only = TRUE ) get_objectives(usable_solutions) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Long format get_objectives( solutions, format = "long" ) # Wide format get_objectives( solutions, format = "wide" ) # Objective values from usable runs only usable_solutions <- solution_filter( solutions, feasible_only = TRUE ) get_objectives(usable_solutions) }
Extract the planning-unit summary table from a solutionset-class
object returned by solve.
The returned table summarizes solution values at the planning-unit level and
typically includes a selected indicator showing whether each planning
unit is selected in a solution.
get_planning_units(x, solution = NULL, ...)get_planning_units(x, solution = NULL, ...)
x |
A |
solution |
Optional positive integer giving the solution id to extract. If
|
... |
Deprecated arguments kept for backwards compatibility. Currently
supports |
This function reads the planning-unit summary stored in
x$summary$pu. It does not reconstruct the table from the raw decision
vector; it simply returns the stored summary after optional run filtering.
Let denote the planning-unit selection variable for planning unit
. In standard multiscape workflows, the selected column
is the user-facing representation of that planning-unit decision, typically
coded as 0 or 1.
If solution is provided, only rows belonging to that solution are returned. This
requires the summary table to contain a solution_id column.
To return only selected planning units, filter the returned table using
selected == 1.
A data.frame containing the stored planning-unit summary.
Typical columns include planning-unit identifiers, optional labels, and a
selected indicator.
get_actions,
get_features,
get_targets
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Planning-unit results for all stored runs get_planning_units(solutions) # Return only selected planning units selected_pu <- get_planning_units(solutions) selected_pu <- selected_pu[selected_pu$selected == 1L, , drop = FALSE] selected_pu # Extract one run using its solution_id solution_ids <- get_runs(solutions)$solution_id get_planning_units( solutions, solution = solution_ids[1] ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Planning-unit results for all stored runs get_planning_units(solutions) # Return only selected planning units selected_pu <- get_planning_units(solutions) selected_pu <- selected_pu[selected_pu$selected == 1L, , drop = FALSE] selected_pu # Extract one run using its solution_id solution_ids <- get_runs(solutions)$solution_id get_planning_units( solutions, solution = solution_ids[1] ) }
get_pu() has been replaced by
get_planning_units.
get_pu(x, solution = NULL, ...)get_pu(x, solution = NULL, ...)
x |
A |
solution |
Optional positive integer giving the solution id to extract. If
|
... |
Deprecated arguments kept for backwards compatibility. Currently
supports |
A data.frame containing the stored planning-unit summary.
Extract the run table from a solutionset-class object.
get_runs(x)get_runs(x)
x |
A |
A run represents an attempted optimization solve. Each run has a unique
run_id. Only runs that produce a stored solution receive a
solution_id.
The solution_id is numeric and matches the corresponding
run_id. Therefore, if a run fails or is infeasible, its
solution_id is NA; if a later run succeeds, its
solution_id keeps the same value as its run_id.
This function is the user-facing place where the relationship between attempted runs and stored solutions is reported.
Objective values are not returned by get_runs(). To extract objective
values, use get_objectives.
A data.frame with one row per attempted optimization run.
The table contains run metadata and the numeric mapping between
run_id and solution_id, but not objective-value columns.
get_objectives,
solution_filter,
set_runs_grid,
set_runs_manual
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) get_runs(solutions) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) get_runs(solutions) }
Convert stored planning-unit/action decisions into one canonical action state per planning unit and solution.
get_solution_states(x)get_solution_states(x)
x |
A |
Selected action names are sorted and joined with "+". Planning units
without a selected action receive the state "unmanaged". The
selected column retains the planning-unit selection indicator when it
is available in the stored planning-unit summary.
A data.frame with columns solution_id, pu,
selected, state, managed, and n_actions.
solution_id is returned as an integer.
get_actions, get_planning_units,
selection_consistency, linkage_transition
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) states <- get_solution_states(solutions) head(states) # Inspect one solution from the returned table. solution_ids <- unique(states$solution_id) states[states$solution_id == solution_ids[1L], ] # Or filter the complete SolutionSet first. one_solution <- solution_filter( solutions, solution_id = solution_ids[1L] ) get_solution_states(one_solution) }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) states <- get_solution_states(solutions) head(states) # Inspect one solution from the returned table. solution_ids <- unique(states$solution_id) states[states$solution_id == solution_ids[1L], ] # Or filter the complete SolutionSet first. one_solution <- solution_filter( solutions, solution_id = solution_ids[1L] ) get_solution_states(one_solution) }
Extract a user-facing target-achievement table from a
solutionset-class object returned by solve.
The returned table summarizes, for each stored target, the target level, the achieved value, the gap between achieved and required values, and whether the target was met in each run.
get_targets(x, solution = NULL, ...)get_targets(x, solution = NULL, ...)
x |
A |
solution |
Optional positive integer giving the solution id to extract. If
|
... |
Deprecated arguments kept for backwards compatibility. Currently
supports |
Targets are optional in multiscape. If the result object does not
contain a targets summary table at x$summary$targets, this function
returns NULL without error.
This function reads the stored targets summary and returns a simplified
user-facing table. If the summary contains achieved and
target_value, target satisfaction is evaluated as follows.
For lower-bound targets:
and for upper-bound targets:
The interpretation of the target direction is taken from the sense
column when available:
"ge", ">=", or "min" are treated as lower-bound
targets;
"le", "<=", or "max" are treated as upper-bound
targets;
if sense is missing, the target is treated as a lower bound by
default.
The returned table is simplified and renames some internal fields for readability:
target_raw is returned as target_level;
basis_total is returned as total_available;
target_value is returned as target.
If solution is provided, only rows belonging to that solution are returned. If
the result contains a run_id column but only a single solution is present and
solution was not requested explicitly, the solution_id column is removed
for convenience.
The gap column is expected to be part of the stored summary. When
present, it typically represents:
A simplified data.frame target summary, or NULL if the
result does not contain targets. Typical columns include feature,
feature_name, target_level, total_available,
target, achieved, gap, and met.
get_planning_units,
get_actions,
get_features
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Target requirements and achieved amounts get_targets(solutions) # Target achievement for one run solution_ids <- get_runs(solutions)$solution_id get_targets( solutions, solution = solution_ids[1] ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Target requirements and achieved amounts get_targets(solutions) # Target achievement for one run solution_ids <- get_runs(solutions)$solution_id get_targets( solutions, solution = solution_ids[1] ) }
Rank pairwise linkage results to identify solution pairs that are especially informative in objective space, decision space, or their relationship.
linkage_contrasts( x, type = c("objective_similar", "decision_similar", "high_turnover", "high_reconfiguration", "low_reconfiguration", "objective_tie"), n = 3L )linkage_contrasts( x, type = c("objective_similar", "decision_similar", "high_turnover", "high_reconfiguration", "low_reconfiguration", "objective_tie"), n = 3L )
x |
A |
type |
Contrast to rank: |
n |
Number of contrasts to return. |
"objective_similar" ranks pairs by increasing objective distance and
uses larger decision distance as a secondary criterion. Conversely,
"decision_similar" ranks pairs by increasing decision distance and
then decreasing objective distance. "high_turnover" ranks pairs by
decreasing decision distance.
The "high_reconfiguration" and "low_reconfiguration" types
require a numeric reconfiguration_rate column, as returned by
linkage_turnover. Rows with an undefined
reconfiguration_rate are excluded from these rankings.
"objective_tie" returns objective-equivalent pairs with non-zero
decision distance, ranked from largest to smallest spatial difference. This
type requires the logical objective_tie column returned by
linkage_turnover. The numerical tolerance used to identify
objective ties is therefore controlled only by linkage_turnover().
from_solution and to_solution must contain positive integer
solution ids. Numeric values such as 1 are accepted when they are
integer-valued and are returned as integers.
The first n ranked rows. The columns in x are
preserved, with contrast_rank and contrast_type prepended to
identify the rank and contrast criterion used.
linkage_distances, linkage_turnover
Other Objective–decision linkage:
linkage_distances(),
linkage_transition(),
linkage_turnover()
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) linkage <- linkage_distances( solutions, objectives = c("cost", "benefit") ) linkage_contrasts( linkage, type = "objective_similar", n = 3 ) turnover <- linkage_turnover( solutions, objectives = c("cost", "benefit") ) linkage_contrasts( turnover, type = "high_reconfiguration", n = 3 ) }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) linkage <- linkage_distances( solutions, objectives = c("cost", "benefit") ) linkage_contrasts( linkage, type = "objective_similar", n = 3 ) turnover <- linkage_turnover( solutions, objectives = c("cost", "benefit") ) linkage_contrasts( turnover, type = "high_reconfiguration", n = 3 ) }
Measure separately how far pairs of stored solutions are in objective space and how much their planning-unit/action assignments differ.
linkage_distances( x, objectives = NULL, pairs = NULL, objective_metric = c("euclidean", "manhattan", "chebyshev"), decision_metric = c("jaccard", "hamming") )linkage_distances( x, objectives = NULL, pairs = NULL, objective_metric = c("euclidean", "manhattan", "chebyshev"), decision_metric = c("jaccard", "hamming") )
x |
A |
objectives |
Optional character vector with two or more unique objective
aliases. If |
pairs |
Either |
objective_metric |
Objective-space distance metric:
|
decision_metric |
Decision-space metric: |
Objective values are oriented to minimization and normalized using the
solutions retained in the supplied SolutionSet. Decision distances
are always calculated on the complete planning-unit/action assignment space
represented by the supplied solutions.
If pairs = NULL, all unordered pairs are generated and oriented from
worse to better on the first selected objective. If pairs is supplied,
its from_solution and to_solution direction is preserved.
Distances are symmetric, but signed objective changes and action additions or
removals depend on pair direction.
No combined linkage score is calculated.
A data.frame with one row per directed pair, including
objective_distance, decision_similarity,
decision_distance, decision-change counts, and objective-specific
from/to values, changes, and improvements.
frontier_neighbors, frontier_distances,
selection_similarity, linkage_transition
Other Objective–decision linkage:
linkage_contrasts(),
linkage_transition(),
linkage_turnover()
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) linkage <- linkage_distances( solutions, objectives = c("cost", "benefit") ) head(linkage) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) linkage_distances( solutions, objectives = c("cost", "benefit"), pairs = neighbors ) }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) linkage <- linkage_distances( solutions, objectives = c("cost", "benefit") ) head(linkage) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) linkage_distances( solutions, objectives = c("cost", "benefit"), pairs = neighbors ) }
Explain the objective and spatial changes required to transform one stored solution into another.
linkage_transition(x, from, to, objectives = NULL)linkage_transition(x, from, to, objectives = NULL)
x |
A |
from |
A single numeric positive-integer id identifying the starting stored solution. |
to |
A single numeric positive-integer id identifying the destination stored solution. |
objectives |
Optional character vector with one or more objective
aliases. If |
The transition is directional. Reversing from and to reverses
signed objective changes, additions and removals, and planning-unit
activations and deactivations. Canonical planning-unit states are obtained
from get_solution_states.
The complete landscape is always returned in transitions, including
planning units whose state is unchanged. To keep the action-level output
compact, actions contains assignments selected in at least one of the
two solutions; added, removed, and retained actions remain explicit.
An object of class multiscape_linkage_transition containing:
summary: the directed solution ids and transition counts;
objectives: objective values and signed changes;
transitions: one row per planning unit in the complete
landscape, including unchanged units;
actions: action assignments selected in either solution;
state_matrix: counts of planning-unit state transitions.
get_solution_states, linkage_distances,
frontier_neighbors
Other Objective–decision linkage:
linkage_contrasts(),
linkage_distances(),
linkage_turnover()
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) transition <- linkage_transition( solutions, from = neighbors$from_solution[1], to = neighbors$to_solution[1], objectives = c("cost", "benefit") ) transition$summary transition$objectives transition$transitions[ transition$transitions$changed, , drop = FALSE ] }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) transition <- linkage_transition( solutions, from = neighbors$from_solution[1], to = neighbors$to_solution[1], objectives = c("cost", "benefit") ) transition$summary transition$objectives transition$transitions[ transition$transitions$changed, , drop = FALSE ] }
Measure decision-space change between selected solution pairs and relate it to their normalized objective-space distance.
linkage_turnover( x, objectives = NULL, pairs = NULL, objective_metric = c("euclidean", "manhattan", "chebyshev"), decision_metric = c("jaccard", "hamming"), tolerance = sqrt(.Machine$double.eps) )linkage_turnover( x, objectives = NULL, pairs = NULL, objective_metric = c("euclidean", "manhattan", "chebyshev"), decision_metric = c("jaccard", "hamming"), tolerance = sqrt(.Machine$double.eps) )
x |
A |
objectives |
Optional character vector with two or more unique
objective aliases. If |
pairs |
Either |
objective_metric |
Objective-space metric: |
decision_metric |
Decision-space metric over the complete
planning-unit/action assignment space: |
tolerance |
A single finite non-negative number. Pairs whose normalized objective distance is less than or equal to this value are treated as objective ties. |
Decision turnover is the decision_distance returned by
linkage_distances. The reconfiguration rate relates that
turnover to normalized objective-space separation:
It is a unitless ratio, not a temporal rate or a percentage of landscape
area. No additional turnover column is returned because it would
duplicate decision_distance.
A pair is an objective tie when objective_distance <= tolerance.
Tied pairs receive NA_real_ for reconfiguration_rate,
including pairs with identical decisions, avoiding undefined or infinite
ratios. Non-tied pairs with identical decisions receive a rate of zero.
If pairs = NULL, neighboring pairs are generated with
frontier_neighbors using method = "auto" and the
requested objective_metric. Supplied pairs preserve their direction.
Reconfiguration rates should be compared only among analyses using the same
objectives, supplied SolutionSet, normalization basis, and distance
metrics.
A data.frame extending the output of
linkage_distances with logical objective_tie and
numeric reconfiguration_rate columns. Decision turnover is the
existing decision_distance column.
frontier_neighbors, linkage_distances,
linkage_contrasts
Other Objective–decision linkage:
linkage_contrasts(),
linkage_distances(),
linkage_transition()
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) turnover <- linkage_turnover( solutions, objectives = c("cost", "benefit"), pairs = neighbors ) turnover turnover[ turnover$objective_tie & turnover$decision_distance > 0, , drop = FALSE ] }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) neighbors <- frontier_neighbors( solutions, objectives = c("cost", "benefit") ) turnover <- linkage_turnover( solutions, objectives = c("cost", "benefit"), pairs = neighbors ) turnover turnover[ turnover$objective_tie & turnover$decision_distance > 0, , drop = FALSE ] }
Load the paired planning units and feature raster used by the integrated planning vignette. This is a different landscape from both the compact simulated example and Meseta. The vignette derives its feature catalogue and reference amounts from the raster.
load_ecosystem_services()load_ecosystem_services()
A named list containing:
An sf object with 30,496 planning units, including
cost, area and historical lock columns.
A terra::SpatRaster with four ecosystem-service layers.
services <- load_ecosystem_services() nrow(services$planning_units) names(services$feature_raster)services <- load_ecosystem_services() nrow(services$planning_units) names(services$feature_raster)
Load the complete inputs used by the introductory README tutorial. These earlier available data support an adaptation of the Meseta case study, not an exact reproduction of the final 2025 article.
load_meseta()load_meseta()
The bundled inputs are redistributed under GPL-3.0. Their source and
preparation notes are recorded in provenance.
The action identifiers in actions$id, action_costs$action and
outcomes$action consistently use Afforestation, BAU, FarmReturn
and Firesmart. The tables can be passed directly to add_actions() and
add_effects(), without recoding identifiers. Scripts that explicitly
select numeric action identifiers must use these names instead.
Original identifiers remain available in provenance$action_id_lookup.
All amounts, costs, targets, reference values and geometries are preserved.
Planning-unit costs are retained; use include_pu_cost = FALSE in
add_objective_min_cost() to count only action implementation costs.
Maps are derived when loading, without storing additional copies of
geometry in package data.
Map columns use the names in actions$name; rows follow planning_units
order, and row names contain planning-unit identifiers.
The five feature maps illustrate contrasting action scenarios (three species and two ecosystem-service features), not ecological importance. Missing outcome rows are displayed as zero, following the dataset convention. These maps describe inputs under each action, not optimized selections.
A named list containing:
An sf object with 11,109 planning units.
A catalogue of 155 features.
The explicit comparison reference used in the tutorial.
A catalogue of four management strategies, with readable
character identifiers in id (also provided in name).
Costs by planning unit and action.
Outcomes by planning unit, action and feature.
Representation targets for all features.
The supplied weighted spatial relation. Replacing it with a newly derived boundary can change the optimization problem.
Data sources and preparation notes.
action_id_lookup records the original numeric source_id and the
returned character id for each action.
Ready-to-plot sf views: costs, DENMINO, CISJUNC,
MLE, AGRICULTURE and ROS. Each view contains four strategy columns
and geometry, so plot() displays the four action scenarios directly.
meseta <- load_meseta() names(meseta) meseta$actions plot(meseta$maps$costs) plot(meseta$maps$DENMINO)meseta <- load_meseta() names(meseta) meseta$actions plot(meseta$maps$costs) plot(meseta$maps$DENMINO)
Load the example feature raster shipped with the package. This access is
retained for compatibility; use load_ecosystem_services() to load its
matching planning units and raster together.
load_sim_features_raster()load_sim_features_raster()
A terra::SpatRaster.
Load a compact, deterministic dataset for examples of multi-objective spatial planning. The landscape contains 64 square planning units, two spatially structured features, and two mutually exclusive candidate actions.
load_sim_multiaction()load_sim_multiaction()
A named list containing:
planning_unitsAn sf object with 64 planning units.
featuresA feature catalogue.
dist_featuresFeature amounts by planning unit.
actionsThe action catalogue.
action_costsAction costs by planning unit and action.
effect_assumptionsRelative-change assumptions by action and feature.
effectsExplicit signed changes by planning unit, action, and feature.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() names(example_data) utils::head(example_data$planning_units)# Load a complete simulated planning problem. example_data <- load_sim_multiaction() names(example_data) utils::head(example_data$planning_units)
mo_control() has been replaced by set_runs_control.
The argument slack_upper_bound is deprecated and ignored. For
AUGMECON, use the slack_upper_bound argument directly in
set_method_augmecon.
mo_control( stop_on_infeasible = FALSE, stop_on_no_solution = FALSE, stop_on_error = TRUE, slack_upper_bound = NULL )mo_control( stop_on_infeasible = FALSE, stop_on_no_solution = FALSE, stop_on_error = TRUE, slack_upper_bound = NULL )
stop_on_infeasible |
Logical. If |
stop_on_no_solution |
Logical. If |
stop_on_error |
Logical. If |
slack_upper_bound |
Deprecated. Use |
An object of class RunsControl, MOControl, and
list.
set_runs_control,
set_method_augmecon
Plot the spatial distribution of selected actions from a
solutionset-class object returned by solve.
This function maps the selected planning unit–action pairs returned by
get_actions onto the planning-unit geometry stored in the associated
Problem object.
plot_spatial_actions( x, solutions = NULL, actions = NULL, layout = NULL, max_facets = 4L, nrow = NULL, ncol = NULL, ..., base_alpha = 0.08, selected_alpha = 0.95, base_fill = "grey95", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE, fill_values = NULL, fill_na = "grey80", use_viridis = TRUE )plot_spatial_actions( x, solutions = NULL, actions = NULL, layout = NULL, max_facets = 4L, nrow = NULL, ncol = NULL, ..., base_alpha = 0.08, selected_alpha = 0.95, base_fill = "grey95", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE, fill_values = NULL, fill_na = "grey80", use_viridis = TRUE )
x |
A |
solutions |
Optional integer vector of solution ids. If |
actions |
Optional action subset to display. Entries may match action ids or action-set labels. |
layout |
Character string controlling the layout. Must be one of
|
max_facets |
Maximum number of action facets shown when |
nrow, ncol
|
Optional numbers of rows and columns used to arrange facets. |
... |
Reserved for future extensions. |
base_alpha |
Numeric value in |
selected_alpha |
Numeric value in |
base_fill |
Fill colour for the base planning-unit layer. |
base_color |
Border colour for the base planning-unit layer. |
selected_color |
Border colour for highlighted layers. |
draw_borders |
Logical. If |
show_base |
Logical. If |
fill_values |
Optional named vector of colours for discrete action maps. |
fill_na |
Fill colour for missing values. |
use_viridis |
Logical. If |
Let denote whether action is selected in
planning unit . This function plots the selected
(pu, action) pairs in geographic space.
If layout = "facet" and only one run is plotted, one panel is drawn
per action.
If layout = "single", all selected actions are drawn in a single map
using discrete fills. If more than one action is selected in the same
planning unit, the action labels are collapsed using "+".
When plotting multiple runs, only layout = "single" is supported.
Planning-unit geometry must be available in the associated problem object.
Invisibly returns a ggplot object.
get_actions,
plot_spatial_planning_units,
plot_spatial_features
if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_actions( solutions, layout = "single" ) }if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_actions( solutions, layout = "single" ) }
Plot feature values in space from a solutionset-class object
returned by solve.
This function combines baseline feature amounts from the associated
Problem object with positive effects induced by the actions selected
in each stored run to produce planning-unit-level feature maps. Selected
actions are obtained through get_actions.
plot_spatial_features( x, solutions = NULL, features = NULL, value = c("final", "baseline", "benefit"), layout = NULL, max_facets = 4L, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE, fill_na = "grey80", use_viridis = TRUE )plot_spatial_features( x, solutions = NULL, features = NULL, value = c("final", "baseline", "benefit"), layout = NULL, max_facets = 4L, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE, fill_na = "grey80", use_viridis = TRUE )
x |
A |
solutions |
Optional integer vector of solution ids. If |
features |
Optional feature subset to display. Matching is attempted against both feature ids and feature names. |
value |
Character string indicating which feature quantity to plot. Must
be one of |
layout |
Character string controlling the layout. Must be one of
|
max_facets |
Maximum number of feature facets shown when
|
... |
Reserved for future extensions. |
base_alpha |
Unused in the current feature view, kept for interface consistency. |
selected_alpha |
Unused in the current feature view, kept for interface consistency. |
base_fill |
Unused in the current feature view, kept for interface consistency. |
base_color |
Unused in the current feature view, kept for interface consistency. |
selected_color |
Border colour for filled feature polygons. |
draw_borders |
Logical. If |
show_base |
Unused in the current feature view, kept for interface consistency. |
fill_na |
Fill colour for missing values. |
use_viridis |
Logical. If |
For each planning unit and feature , the plotted quantities
are:
In the current implementation:
baseline is the summed baseline amount from
dist_features;
benefit is the summed positive effect from selected actions;
final is baseline + benefit.
Negative effects are not subtracted in this plotting method. Therefore,
value = "final" should be interpreted as baseline plus selected
positive effects under the current plotting logic.
If layout = "facet" and only one run is plotted, one panel is drawn
per feature.
If multiple runs are plotted, exactly one feature must be requested, and faceting is done by run.
Planning-unit geometry must be available in the associated problem object.
Invisibly returns a ggplot object.
get_features,
plot_spatial_planning_units,
plot_spatial_actions
if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_features( solutions, features = 1, value = "final" ) }if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_features( solutions, features = 1, value = "final" ) }
Plot the spatial distribution of selected planning units from a
solutionset-class object returned by solve.
This function maps the planning-unit selection summary returned by
get_planning_units onto the planning-unit geometry stored in
the associated Problem object.
plot_spatial_planning_units( x, solutions = NULL, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE )plot_spatial_planning_units( x, solutions = NULL, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE )
x |
A |
solutions |
Optional integer vector of solution ids. If |
... |
Reserved for future extensions. |
base_alpha |
Numeric value in |
selected_alpha |
Numeric value in |
base_fill |
Fill colour for the base planning-unit layer. |
base_color |
Border colour for the base planning-unit layer. |
selected_color |
Border colour for selected planning units. |
draw_borders |
Logical. If |
show_base |
Logical. If |
Let denote the planning-unit selection variable for
planning unit . This function plots the user-facing
selected == 1 representation of .
If several runs are requested, the output is faceted by run_id.
Planning-unit geometry must be available in the associated problem object.
Invisibly returns a ggplot object.
get_planning_units,
plot_spatial_actions,
plot_spatial_features,
plot_spatial_planning_units
if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_planning_units(solutions) }if ( requireNamespace("sf", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_spatial_planning_units(solutions) }
plot_spatial_pu() has been replaced by
plot_spatial_planning_units.
plot_spatial_pu( x, solutions = NULL, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE )plot_spatial_pu( x, solutions = NULL, ..., base_alpha = 0.1, selected_alpha = 0.9, base_fill = "grey92", base_color = NA, selected_color = NA, draw_borders = FALSE, show_base = TRUE )
x |
A |
solutions |
Optional integer vector of solution ids. If |
... |
Reserved for future extensions. |
base_alpha |
Numeric value in |
selected_alpha |
Numeric value in |
base_fill |
Fill colour for the base planning-unit layer. |
base_color |
Border colour for the base planning-unit layer. |
selected_color |
Border colour for selected planning units. |
draw_borders |
Logical. If |
show_base |
Logical. If |
Invisibly returns a ggplot object.
Plot pairwise trade-offs among objective values stored in a
solutionset-class object.
This function is intended for workflows in which the solution set contains
one row per run and two or more objective-value columns of the form
value_*.
If exactly two objectives are selected, the function returns a single scatterplot. If three or more objectives are selected, all pairwise combinations are plotted using facets.
plot_tradeoff( x, objectives = NULL, color_by = NULL, all_pairs = NULL, connect = FALSE, label_runs = FALSE, point_size = 3, line_alpha = 0.5, text_size = 3, ... )plot_tradeoff( x, objectives = NULL, color_by = NULL, all_pairs = NULL, connect = FALSE, label_runs = FALSE, point_size = 3, line_alpha = 0.5, text_size = 3, ... )
x |
A |
objectives |
Optional character vector of objective aliases to display.
These must match the suffixes of the |
color_by |
Optional character scalar used to colour points. This may be
either one of the selected objective aliases or one of the run-level
columns |
all_pairs |
Logical. If |
connect |
Logical. If |
label_runs |
Logical. If |
point_size |
Numeric point size. |
line_alpha |
Numeric alpha value for connecting lines. |
text_size |
Numeric size for run labels. |
... |
Reserved for future extensions. |
This function reads the run-level table stored in x$solution$runs. It
expects objective values to be stored in columns whose names begin with
"value_".
If the available objective columns are, for example,
value_cost, value_benefit, and value_frag, then the
corresponding objective aliases are "cost", "benefit", and
"frag".
Let denote the value of objective in run . This
function visualizes pairwise projections of the run table of the form:
for selected pairs of objectives .
If exactly two objectives are selected, a single panel is produced.
If three or more objectives are selected, all pairwise combinations are generated:
where is the selected set of objective aliases.
By default, plotting more than four objectives is not allowed unless
all_pairs = TRUE, because the number of panels grows quadratically in
the number of objectives.
Colouring
If color_by is supplied, points are coloured by either:
one of the selected objective aliases, in which case the
corresponding value_* column is used;
or one of the run-level columns run_id, status,
runtime, or gap.
Connecting runs
If connect = TRUE, runs are connected in their current table order
within each panel. This can be useful when runs correspond to an ordered scan
of weights, -levels, or frontier points, but it should be used
with care when run order has no substantive meaning.
Run labels
If label_runs = TRUE, each point is labelled by its run_id. If
the ggrepel package is available, repelled labels are used.
Invisibly returns a ggplot object.
if ( requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_tradeoff( solutions, objectives = c("cost", "benefit") ) }if ( requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("rcbc", quietly = TRUE) ) { # Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) |> set_solver_cbc(verbose = FALSE) solutions <- solve(problem) plot_tradeoff( solutions, objectives = c("cost", "benefit") ) }
The Problem class is the central container used by multiscape to
represent a planning problem before, during, and after model construction.
A Problem object stores the full problem specification in a modular
way. This includes the baseline planning data, optional spatial metadata,
action definitions, effects, profit, targets, constraints, objective
registrations, solver settings, and, when available, a built optimization
model or model snapshot.
In other words, Problem is the persistent object that connects the full
multiscape workflow:
create_problem() -> add_*() / set_*() -> solve()
Repeated calls
Action catalogues, effects, profit, solver configuration, multi-objective methods, and unaliased single objectives can each be defined once. A second definition raises an error. Action sets, named spatial relations, aliased objectives, and distinct constraints accumulate; duplicate definitions are rejected. Locks accumulate compatible states, allow identical repeats, and reject contradictory states. To compare scenarios or configurations, derive each alternative from a common problem before the relevant definition.
Conceptual role
The Problem class is designed for a data-first and modular workflow.
User-facing functions do not usually modify a solver object directly. Instead,
they enrich the Problem object by storing new specifications in its
internal data field.
Thus, a Problem object should be understood as a structured container
for the mathematical planning problem, not necessarily as a built
optimization model.
Before solve is called, the object may contain only input data
and user specifications. During or after solving, it may additionally contain
a built model pointer, model metadata, and solver-related information.
Core mathematical interpretation
At a high level, the Problem object stores the ingredients required to
define an optimization problem over:
planning units ,
features ,
actions ,
optional spatial relations over planning units,
and user-defined objectives and constraints.
The baseline ecological state is typically stored through a planning unit–feature table of amounts:
where is the baseline amount of feature in planning unit
.
Subsequent functions then add action feasibility, effects, profit, targets, spatial relations, and optimization settings to this baseline representation.
How objects are created
Problem objects are usually created by create_problem.
After creation, downstream functions such as add_actions,
add_effects, add_profit,
add_constraint_targets_absolute, add_constraint_targets_relative,
spatial relation constructors, objective setters, and solver setters extend
the internal data list.
Internal storage
The class contains a single field:
dataA named list storing the full problem
specification, metadata, and, when available, built-model information.
Common entries of data include:
puPlanning-unit table.
featuresFeature table.
actionsAction catalog.
action_setsNamed combinations of actions, stored as a long table of set–action memberships. These definitions do not impose joint selection or change the optimization model.
dist_featuresPlanning unit–feature baseline amounts.
dist_actionsFeasible planning unit–action pairs.
dist_effectsAction effects by planning unit, action, and feature.
dist_profitProfit by planning unit–action pair.
pu_sfPlanning-unit geometry when available.
pu_coordsPlanning-unit coordinates when available.
spatial_relationsRegistered spatial relations.
targetsStored target specifications.
constraintsStored user-defined constraints, including
per-unit action counts in constraints$action_cardinality and logical
relations in constraints$action_relations.
effects_original, joint_effects, effect_terms
Canonical supplied totals, supplied joint totals, and their signed subset
corrections, when registered sets occur in modern effect input. Missing
interactions are assumed zero as recorded in effects_meta.
objectivesRegistered atomic objectives for single- or multi-objective workflows.
methodStored multi-objective method configuration, when applicable.
solve_argsStored solver settings.
model_ptrPointer to a built optimization model, when available.
model_argsMetadata describing model construction.
model_listOptional exported model snapshot or representation.
metaAuxiliary metadata, including model-dirty flags and other bookkeeping fields.
Not every Problem object contains all of these entries. The content of
data depends on how far the workflow has progressed.
Lifecycle
A Problem object typically moves through the following stages:
input stage: baseline planning units, features, and feature distributions are stored,
specification stage: actions, effects, targets, objectives, constraints, and spatial relations are added,
model stage: an optimization model is built from the stored specification,
solve stage: the model is solved and results are returned in a
separate Solution or SolutionSet object.
The Problem object itself is not the solution. It is the structured
problem definition from which a solution can be obtained.
No return value. This page documents the Problem class.
print()Print a structured summary of the stored problem specification, including data, actions and effects, spatial inputs, targets and constraints, and model status. If a model has already been built, additional dimensions and auxiliary-variable information are displayed.
show()Alias of print().
repr()Return a short one-line representation of the object.
getData(name)Return a named entry from self$data.
getPlanningUnitsAmount()Return the number of planning units
stored in x$data$pu.
getMonitoringCosts()Return the planning-unit cost vector,
typically taken from x$data$pu$cost.
getFeatureAmount()Return the number of stored features.
getFeatureNames()Return feature names from
x$data$features$name, or feature ids if names are unavailable.
getActionCosts()Return action-level costs from
x$data$dist_actions$cost when available.
getActionsAmount()Return the number of stored actions.
The print() method is intended as a quick diagnostic summary. It helps
users understand:
what data have already been loaded,
whether actions, effects, spatial relations, targets, and constraints have been registered,
whether objectives and methods have been configured,
whether a model has already been built,
and whether the object appears ready to be solved.
In particular, the model section of the printed output summarizes whether the current problem specification is incomplete, ready, or already materialized as a built optimization model.
create_problem,
add_actions,
add_action_sets,
add_constraint_action_cardinality,
add_effects,
add_constraint_targets_absolute,
solve
run_grid() has been replaced by set_runs_grid.
The argument include_extremes is deprecated and ignored. Boundary
levels are now always included.
run_grid(n, include_extremes = TRUE)run_grid(n, include_extremes = TRUE)
n |
Integer. Resolution of the automatically generated run design.
Must be at least |
include_extremes |
Deprecated. Boundary levels are now always included. |
An object of class RunGrid and RunDesign.
run_manual() has been replaced by set_runs_manual.
run_manual(x)run_manual(x)
x |
A non-empty |
An object of class RunManual and RunDesign.
Summarize recurrence and disagreement of canonical planning-unit states across stored solutions.
selection_consistency(x, solution_groups = NULL)selection_consistency(x, solution_groups = NULL)
x |
A |
solution_groups |
Optional named list assigning every stored numeric
solution id to exactly one group. If |
States are obtained from get_solution_states. All stored
solutions contribute equally, and all planning units are returned.
solution_groups can partition the stored solutions into named groups;
consistency is then summarized separately within each group.
Frequencies describe the proportion of stored solutions in the corresponding
group. They are conditional summaries of the supplied SolutionSet, not
probabilities or formal measures of uncertainty. variable is
TRUE when more than one state occurs for a planning unit within a
solution group.
A data.frame with one row per planning unit and solution
group, including selection, managed and unmanaged frequencies, dominant
state and frequency, number of observed states, whether the unit is
variable within the group, Shannon entropy, and normalized entropy.
get_solution_states, selection_frequency,
selection_similarity, solution_filter
# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) consistency <- selection_consistency(solutions) head(consistency) solution_ids <- get_runs(solutions)$solution_id solution_ids <- solution_ids[!is.na(solution_ids)] if (length(solution_ids) >= 2L) { cut <- ceiling(length(solution_ids) / 2) groups <- list( first = solution_ids[seq_len(cut)], second = solution_ids[seq.int(cut + 1L, length(solution_ids))] ) selection_consistency( solutions, solution_groups = groups ) } }# Load a complete simulated multi-action problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost( alias = "cost", include_pu_cost = FALSE ) |> add_objective_max_effect( alias = "benefit" ) |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 5), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc(problem, verbose = FALSE) solutions <- solve(problem) consistency <- selection_consistency(solutions) head(consistency) solution_ids <- get_runs(solutions)$solution_id solution_ids <- solution_ids[!is.na(solution_ids)] if (length(solution_ids) >= 2L) { cut <- ceiling(length(solution_ids) / 2) groups <- list( first = solution_ids[seq_len(cut)], second = solution_ids[seq.int(cut + 1L, length(solution_ids))] ) selection_consistency( solutions, solution_groups = groups ) } }
Calculate how frequently each planning-unit/action assignment is selected
across the stored solutions in a
solutionset-class object.
selection_frequency(x)selection_frequency(x)
x |
A |
Selection frequency is calculated at the planning-unit/action level. This is the canonical decision representation used by this function because it preserves differences between solutions that select the same planning unit but assign different actions.
For each planning-unit/action pair, the frequency is:
where equals one when planning unit receives action
in solution , and zero otherwise.
The result is computed over all stored solutions in the supplied
SolutionSet. To calculate frequencies for only a subset of solutions,
first use solution_filter or
solution_unique.
For simple conservation-planning problems without explicit actions,
selected planning units are represented using the canonical action name
"conservation".
Selection frequency measures recurrence across the supplied solutions. It should not automatically be interpreted as formal irreplaceability because it depends on the solutions included, their sampling across objective space, and whether duplicate or dominated solutions have been retained.
A data.frame with one row per planning-unit/action pair and
the following columns:
pu: planning-unit identifier;
action: action identifier or name;
n_selected: number of stored solutions in which the
planning-unit/action pair is selected;
n_solutions: total number of stored solutions considered;
frequency: proportion of stored solutions in which the pair
is selected.
selection_similarity,
selection_consistency,
solution_filter,
solution_unique,
get_actions,
get_planning_units
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Frequency across all stored solutions frequency <- selection_frequency(solutions) frequency # Restrict the analysis to non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) selection_frequency(nondominated_solutions) } # Give each distinct decision configuration the same weight unique_solutions <- solution_unique( solutions, by = "decisions" ) unique_frequency <- selection_frequency( unique_solutions ) unique_frequency }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Frequency across all stored solutions frequency <- selection_frequency(solutions) frequency # Restrict the analysis to non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) selection_frequency(nondominated_solutions) } # Give each distinct decision configuration the same weight unique_solutions <- solution_unique( solutions, by = "decisions" ) unique_frequency <- selection_frequency( unique_solutions ) unique_frequency }
Calculate pairwise structural similarity among the stored solutions in a
solutionset-class object.
selection_similarity( x, metric = c("jaccard", "hamming"), format = c("long", "matrix") )selection_similarity( x, metric = c("jaccard", "hamming"), format = c("long", "matrix") )
x |
A |
metric |
Character. Similarity metric to use. One of
|
format |
Character. Output format. If |
Solutions are compared using their complete planning-unit/action assignment vectors. Consequently, two solutions that select the same planning unit but assign different actions are treated as structurally different.
For simple conservation-planning problems without explicit actions,
selected planning units are represented using the canonical action name
"conservation".
Two similarity metrics are supported:
"jaccard" compares the sets of selected planning-unit/action
assignments:
Jaccard similarity focuses on selected assignments and ignores joint absences. It is generally the preferred metric for sparse conservation and management portfolios.
"hamming" calculates the proportion of decision-vector
positions that are equal:
where is the number of feasible planning-unit/action assignments.
Unlike Jaccard similarity, Hamming similarity includes shared
non-selections.
For both metrics, similarity ranges from zero to one:
1 indicates identical assignment structures;
0 indicates no structural agreement under the selected
metric.
The corresponding distance is calculated as:
The comparison is performed over all stored solutions in the supplied
object. To compare only a subset, first use
solution_filter or solution_unique.
The two solution-id columns are returned as integers.
If format = "long", a data.frame with columns:
solution_id_1;
solution_id_2;
similarity;
distance.
If format = "matrix", a symmetric numeric matrix of similarities is
returned. Its diagonal is equal to one.
The selected metric is stored in the "metric" attribute.
selection_frequency,
selection_consistency,
solution_filter,
solution_unique,
get_actions,
get_planning_units
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Pairwise Jaccard similarity in long format jaccard_long <- selection_similarity( solutions ) jaccard_long # Symmetric Jaccard similarity matrix jaccard_matrix <- selection_similarity( solutions, format = "matrix" ) jaccard_matrix # Hamming similarity includes shared non-selections hamming_long <- selection_similarity( solutions, metric = "hamming" ) hamming_long # Compare only structurally unique solutions unique_solutions <- solution_unique( solutions, by = "decisions" ) selection_similarity( unique_solutions, format = "matrix" ) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Pairwise Jaccard similarity in long format jaccard_long <- selection_similarity( solutions ) jaccard_long # Symmetric Jaccard similarity matrix jaccard_matrix <- selection_similarity( solutions, format = "matrix" ) jaccard_matrix # Hamming similarity includes shared non-selections hamming_long <- selection_similarity( solutions, metric = "hamming" ) hamming_long # Compare only structurally unique solutions unique_solutions <- solution_unique( solutions, by = "decisions" ) selection_similarity( unique_solutions, format = "matrix" ) }
Explore trade-offs among registered objectives using AUGMECON. One objective is optimised directly, while the others are constrained by epsilon levels and contribute small, normalised improvement terms to discourage weakly efficient solutions.
set_method_augmecon( x, primary, aliases = NULL, runs = NULL, grid = NULL, n_points = NULL, include_extremes = NULL, lexicographic = TRUE, lexicographic_tol = 1e-09, augmentation = 0.001, slack_upper_bound = 1e+06, control = NULL )set_method_augmecon( x, primary, aliases = NULL, runs = NULL, grid = NULL, n_points = NULL, include_extremes = NULL, lexicographic = TRUE, lexicographic_tol = 1e-09, augmentation = 0.001, slack_upper_bound = 1e+06, control = NULL )
x |
A |
primary |
Character scalar naming the objective optimised directly. |
aliases |
Optional character vector of registered objective aliases.
By default, all registered objectives are included; |
runs |
A run design from |
grid, n_points, include_extremes
|
Deprecated run-design arguments;
use |
lexicographic |
Logical. Whether to use lexicographic anchoring when
computing automatic-grid extreme points. Defaults to |
lexicographic_tol |
Non-negative tolerance for the lexicographic extreme-point calculation, in the objective's original units. |
augmentation |
Positive coefficient for the normalised slack reward.
Defaults to |
slack_upper_bound |
Positive upper bound for AUGMECON slack variables.
Defaults to |
control |
Optional run-failure settings from |
Register at least two objectives with distinct alias values. The
primary objective is optimised directly; the remaining objectives
selected by aliases become epsilon-constrained objectives. For example,
with woodland improvement as the primary objective and cost as secondary,
each run seeks the greatest improvement while keeping cost below a ceiling:
What augmentation changes. Ordinary epsilon-constraint may return a
weakly efficient plan when another objective could improve without changing
the primary objective. AUGMECON introduces a non-negative slack for every
secondary constraint and rewards normalised slack through a small positive
coefficient augmentation (Mavrotas, 2009). For a minimised cost objective,
the slack measures how far actual cost lies below its epsilon ceiling.
Schematically, with a maximised primary objective, AUGMECON uses
where is augmentation, is the non-negative slack,
and is the payoff-table range of the corresponding secondary
objective. The implementation handles the signs of minimisation and
maximisation objectives internally. Choose a small augmentation so that
secondary improvements do not unintentionally outweigh primary performance.
Automatic epsilon grids. Use runs = set_runs_grid(n = 5) to obtain
five epsilon levels, including the endpoints, for each secondary objective.
Levels and payoff-table ranges are calculated during solve().
With three objectives, for example, two secondary grids of five levels
yield up to 25 combinations. When lexicographic = TRUE, extreme-point
calculations use lexicographic anchoring, with lexicographic_tol
specifying the tolerance.
Manual thresholds. Use runs = set_runs_manual() to supply explicit
epsilon values, one row per run and one column named eps_<alias> for each
secondary objective. For a minimised cost objective, eps_cost = 20 means
cost at most 20, not exactly 20. Unlike automatic grids, manual rows are
not expanded into all possible combinations; use expand.grid() if needed.
Interpreting solutions. The augmentation reduces weak-efficiency
problems, but a finite epsilon grid does not enumerate every Pareto-optimal
plan, and different runs can produce identical spatial decisions. Numerical
tolerances and finite solver time limits also matter. A numerical refinement
pass may improve secondary slacks after an optimal augmented solve;
solver settings and epsilon-bound checks are available in the solution
diagnostics. slack_upper_bound must be large enough for valid slacks,
without being unnecessarily loose. Use set_runs_control() to manage
infeasible or unsuccessful runs.
Only one multi-objective method can be configured on a Problem; compare
methods by starting from the same problem before calling set_method_*().
The older grid, n_points, and include_extremes arguments are deprecated
in favour of runs.
An updated Problem with the method stored in x$data$method.
Payoff-table calculations and optimisation runs take place in solve().
Mavrotas, G. (2009). Effective implementation of the
-constraint method in multi-objective mathematical
programming problems. Applied Mathematics and Computation, 213(2),
455-465. doi:10.1016/j.amc.2009.03.037.
Mavrotas, G., and Florios, K. (2013). An improved version of the augmented
-constraint method (AUGMECON2) for finding the exact
Pareto set in multi-objective integer programming problems. Applied
Mathematics and Computation, 219(18), 9652-9669.
doi:10.1016/j.amc.2013.03.002. (Related extension; not the same method.)
set_method_epsilon_constraint(), set_method_weighted_sum(),
set_runs_grid(), set_runs_manual(), set_runs_control(),
get_objectives(), plot_tradeoff(), plot_spatial_actions(), solve()
# EXAMPLE: Maximise woodland improvement under alternative cost limits. # Use the same 64-unit landscape and objectives as in the other MO methods. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some improvement to avoid an empty low-cost solution. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_max_effect( features = "woodland", alias = "woodland" ) |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) # Explore five cost ceilings. Small slack rewards help distinguish # plans with similar woodland improvements but different costs. augmecon <- set_method_augmecon( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_grid(n = 5), lexicographic = TRUE, augmentation = 1e-3 ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(augmecon, time_limit = 30, verbose = FALSE) ) # Actual cost may be lower than the epsilon ceiling. get_objectives(solutions, format = "wide") # Inspect the trade-off frontier generated by AUGMECON. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare selected management actions for three epsilon levels. # The three runs need not produce three different spatial plans. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: use three manually specified cost ceilings. # Start again from 'base', before configuring a multi-objective method. manual <- set_method_augmecon( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_manual(data.frame(eps_cost = c(10, 20, 40))), augmentation = 1e-3 )# EXAMPLE: Maximise woodland improvement under alternative cost limits. # Use the same 64-unit landscape and objectives as in the other MO methods. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some improvement to avoid an empty low-cost solution. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_max_effect( features = "woodland", alias = "woodland" ) |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) # Explore five cost ceilings. Small slack rewards help distinguish # plans with similar woodland improvements but different costs. augmecon <- set_method_augmecon( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_grid(n = 5), lexicographic = TRUE, augmentation = 1e-3 ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(augmecon, time_limit = 30, verbose = FALSE) ) # Actual cost may be lower than the epsilon ceiling. get_objectives(solutions, format = "wide") # Inspect the trade-off frontier generated by AUGMECON. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare selected management actions for three epsilon levels. # The three runs need not produce three different spatial plans. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: use three manually specified cost ceilings. # Start again from 'base', before configuring a multi-objective method. manual <- set_method_augmecon( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_manual(data.frame(eps_cost = c(10, 20, 40))), augmentation = 1e-3 )
Optimise one registered objective while imposing performance thresholds on the others. By varying these thresholds, the epsilon-constraint method produces alternative spatial plans along an observed trade-off frontier.
set_method_epsilon_constraint( x, primary, aliases = NULL, runs = NULL, eps = NULL, mode = NULL, n_points = NULL, include_extremes = NULL, lexicographic = TRUE, lexicographic_tol = 1e-08, control = NULL )set_method_epsilon_constraint( x, primary, aliases = NULL, runs = NULL, eps = NULL, mode = NULL, n_points = NULL, include_extremes = NULL, lexicographic = TRUE, lexicographic_tol = 1e-08, control = NULL )
x |
A |
primary |
Character scalar naming the objective to optimise directly. |
aliases |
Optional character vector of registered objective aliases.
By default, all registered objectives are used; |
runs |
A run design from |
eps, mode, n_points, include_extremes
|
Deprecated arguments from the
previous interface. Use |
lexicographic |
Logical. Whether to refine automatic-grid anchor
solutions lexicographically; the default is |
lexicographic_tol |
Non-negative tolerance for lexicographic anchor refinement, expressed in the first objective's original units. |
control |
Optional run-failure settings from |
First, register at least two objectives with distinct alias values.
primary identifies the objective to optimise directly; every other alias
included in aliases becomes an epsilon-constrained objective. For example,
maximising woodland improvement while restricting expenditure gives
subject to
For a constrained maximisation objective, the corresponding requirement is
a lower bound, . Epsilon values are objective
values in their original units, not weights or relative preferences.
Automatic grids. runs = set_runs_grid(n = 5) generates five epsilon
levels, including the two endpoints, from objective extreme-point or payoff
information computed during solve(). Automatic grids currently support
exactly two objectives: one primary and one constrained objective.
With lexicographic = TRUE, the anchor solutions are refined using the
other objective to break near-ties. lexicographic_tol controls the allowed
deterioration of the first objective during that refinement; it is expressed
in that objective's units. This option concerns automatic-grid anchors, not
every subsequent epsilon-constrained run.
Automatic warm starts. Successful solutions are reused as initial candidates for subsequent anchor stages and epsilon runs when supported by the solver (CBC and Gurobi). Automatic grids proceed from the most restrictive secondary bound to the most permissive; manual designs keep their supplied order. A run without a usable solution retains the last successful candidate. This behaviour is internal and requires no additional argument.
Manual thresholds. Use runs = set_runs_manual() to supply explicit
bounds. Each row defines one run, with a column named eps_<alias> for
every constrained objective. For example, eps_cost = 10 means cost at
most 10, even if the optimal plan spends less. Manual designs support
two or more objectives; they do not automatically expand combinations
of epsilon levels. Use expand.grid() to create such a design explicitly.
Interpreting results. The method does not guarantee a different plan
for each epsilon level, nor does a finite grid enumerate the full Pareto
frontier. A basic epsilon-constraint formulation can also produce weakly
efficient solutions when an unconstrained secondary objective could improve
without worsening the primary objective. For augmented tie-breaking, see
set_method_augmecon(). Some requested thresholds may be infeasible; use
set_runs_control() to specify how unsuccessful runs are handled. The
returned solution diagnostics include checks of the epsilon bounds.
Only one multi-objective method can be configured on a Problem. To compare
methods or threshold designs, start again from the problem before its
method was configured. The older eps, mode, n_points, and
include_extremes arguments are deprecated; use runs instead.
An updated Problem with the method stored in x$data$method.
Run-specific epsilon bounds are constructed or read during solve().
Haimes, Y. Y., Lasdon, L. S., and Wismer, D. A. (1971). On a bicriterion formulation of the problems of integrated system identification and system optimization. IEEE Transactions on Systems, Man, and Cybernetics, SMC-1(3), 296-297. doi:10.1109/TSMC.1971.4308298.
Mavrotas, G. (2009). Effective implementation of the epsilon-constraint method in multi-objective mathematical programming problems. Applied Mathematics and Computation, 213(2), 455-465. doi:10.1016/j.amc.2009.03.037.
set_method_weighted_sum(), set_method_augmecon(), set_runs_grid(),
set_runs_manual(), set_runs_control(), get_objectives(),
plot_tradeoff(), plot_spatial_actions(), solve()
# EXAMPLE: Maximise woodland improvement under alternative cost limits. # The landscape has 64 planning units and two candidate actions per unit. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some woodland improvement, so the low-cost plan is not empty. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_max_effect( features = "woodland", alias = "woodland" ) |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) # Generate five cost limits automatically from the objective extremes. epsilon <- set_method_epsilon_constraint( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_grid(n = 5), lexicographic = TRUE ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(epsilon, time_limit = 30, verbose = FALSE) ) # Each run maximises woodland gain subject to its own cost ceiling. get_objectives(solutions, format = "wide") # The horizontal axis shows actual cost, not the epsilon ceiling. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare low-, intermediate- and high-cost spatial prescriptions. # Epsilon levels can sometimes produce the same selected actions. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: specify explicit cost ceilings rather than a grid. # Start from 'base', since each Problem can have only one method. manual <- set_method_epsilon_constraint( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_manual(data.frame(eps_cost = c(10, 20, 40))) )# EXAMPLE: Maximise woodland improvement under alternative cost limits. # The landscape has 64 planning units and two candidate actions per unit. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some woodland improvement, so the low-cost plan is not empty. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_max_effect( features = "woodland", alias = "woodland" ) |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) # Generate five cost limits automatically from the objective extremes. epsilon <- set_method_epsilon_constraint( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_grid(n = 5), lexicographic = TRUE ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(epsilon, time_limit = 30, verbose = FALSE) ) # Each run maximises woodland gain subject to its own cost ceiling. get_objectives(solutions, format = "wide") # The horizontal axis shows actual cost, not the epsilon ceiling. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare low-, intermediate- and high-cost spatial prescriptions. # Epsilon levels can sometimes produce the same selected actions. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: specify explicit cost ceilings rather than a grid. # Start from 'base', since each Problem can have only one method. manual <- set_method_epsilon_constraint( base, primary = "woodland", aliases = c("woodland", "cost"), runs = set_runs_manual(data.frame(eps_cost = c(10, 20, 40))) )
Combine two or more registered objectives using alternative non-negative weights. Each weight combination defines one optimisation run, allowing users to explore trade-offs between objectives such as implementation cost and ecological improvement.
set_method_weighted_sum( x, aliases, runs = NULL, weights = NULL, normalize_weights = NULL, objective_scaling = FALSE, control = NULL )set_method_weighted_sum( x, aliases, runs = NULL, weights = NULL, normalize_weights = NULL, objective_scaling = FALSE, control = NULL )
x |
A |
aliases |
Character vector of at least two distinct objective aliases. |
runs |
A run design from |
weights |
Deprecated numeric weight vector, in |
normalize_weights |
Logical or |
objective_scaling |
Logical. Request scaling of objective values before
weighted aggregation. The default is |
control |
Optional run-failure settings from |
First, register at least two objectives with distinct alias values.
The aliases argument selects which objectives to combine. For each
run, multiscape constructs a weighted scalar criterion and solves the
resulting mixed-integer optimisation problem.
If is a registered objective and is its weight,
the scalarisation combines terms of the form . Minimisation
and maximisation directions are reconciled internally: larger values remain
preferable for maximisation objectives, smaller values for minimisation
objectives. Weights express preferences, not objective values or targets.
Selecting weights. Use runs = set_runs_grid(n = 5) to
generate five combinations for two objectives, including the two single-
objective extremes. Use set_runs_manual() to provide exact weights
in columns named weight_<alias>; each row is a separate run.
Normalisation versus objective scaling. normalize_weights
rescales the weights within each run to sum to one. By default, grid weights
are normalised, whereas manual weights are retained as supplied. This does
not make objectives with different units numerically comparable.
objective_scaling = TRUE instead requests scaling of the objective
values before aggregation, so the preferences are less sensitive to their
numerical ranges. The scaling is performed when solving; reported objective
values retain their original units.
Interpreting solutions. Different weights can produce the same
spatial plan, particularly in discrete optimisation problems. Weighted sums
generally recover supported efficient solutions; they need not find all
non-dominated solutions of a non-convex frontier. For a more systematic
exploration, consider set_method_epsilon_constraint() or
set_method_augmecon(). Zero-weight runs may also have ties that are
not resolved with respect to the omitted objective.
The control argument specifies how to handle unsuccessful runs. A
problem can have only one configured multi-objective method; to compare
methods or weight designs, begin from the same problem before calling a
set_method_*() function. The deprecated weights argument is
still accepted for compatibility, but runs is recommended.
An updated Problem with the method stored in
x$data$method. The optimisation runs are executed by solve().
Ehrgott, M. (2005). Multicriteria Optimization, 2nd ed. Springer. doi:10.1007/3-540-27659-9.
Miettinen, K. (1998). Nonlinear Multiobjective Optimization. Kluwer Academic Publishers. doi:10.1007/978-1-4615-5563-6.
set_runs_grid, set_runs_manual,
set_runs_control, set_method_epsilon_constraint,
set_method_augmecon, plot_tradeoff,
plot_spatial_actions, solve
# EXAMPLE: Trade off management costs against woodland improvement. # The simulated landscape has 64 planning units and two possible actions. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some improvement, so the cheapest plan is not empty. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) |> add_objective_max_effect( features = "woodland", alias = "woodland" ) # Five different preferences, from cost-only to woodland-only. weighted <- set_method_weighted_sum( base, aliases = c("cost", "woodland"), runs = set_runs_grid(n = 5), objective_scaling = TRUE ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(weighted, time_limit = 30, verbose = FALSE) ) # Inspect the original objective values across runs. get_objectives(solutions, format = "wide") # Explore the observed cost-improvement trade-off. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare three spatial prescriptions in the same figure. # These are preferences, not necessarily three distinct plans. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: supply three explicit weight combinations. # Reuse 'base', because a Problem can have only one method. manual <- set_method_weighted_sum( base, aliases = c("cost", "woodland"), runs = set_runs_manual(data.frame( weight_cost = c(1, 0.5, 0), weight_woodland = c(0, 0.5, 1) )), objective_scaling = TRUE )# EXAMPLE: Trade off management costs against woodland improvement. # The simulated landscape has 64 planning units and two possible actions. sim <- load_sim_multiaction() base <- create_problem( pu = sim$planning_units, features = sim$features, dist_features = sim$dist_features, cost = "cost" ) |> add_actions(sim$actions, cost = sim$action_costs) |> add_effects(sim$effect_assumptions) |> # Require some improvement, so the cheapest plan is not empty. add_constraint_targets_relative(0.05, features = "woodland") |> add_objective_min_cost( include_pu_cost = FALSE, alias = "cost" ) |> add_objective_max_effect( features = "woodland", alias = "woodland" ) # Five different preferences, from cost-only to woodland-only. weighted <- set_method_weighted_sum( base, aliases = c("cost", "woodland"), runs = set_runs_grid(n = 5), objective_scaling = TRUE ) if (requireNamespace("rcbc", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { solutions <- solve( set_solver_cbc(weighted, time_limit = 30, verbose = FALSE) ) # Inspect the original objective values across runs. get_objectives(solutions, format = "wide") # Explore the observed cost-improvement trade-off. plot_tradeoff( solutions, objectives = c("cost", "woodland"), label_runs = TRUE ) # Compare three spatial prescriptions in the same figure. # These are preferences, not necessarily three distinct plans. plot_spatial_actions( solutions, solutions = c(1, 3, 5), layout = "single", ncol = 3 ) } # ALTERNATIVE: supply three explicit weight combinations. # Reuse 'base', because a Problem can have only one method. manual <- set_method_weighted_sum( base, aliases = c("cost", "woodland"), runs = set_runs_manual(data.frame( weight_cost = c(1, 0.5, 0), weight_woodland = c(0, 0.5, 1) )), objective_scaling = TRUE )
Create a control object that determines how multi-objective workflows respond to infeasible runs, missing solution vectors, and unexpected errors.
The resulting object is supplied to the control argument of
set_method_epsilon_constraint or
set_method_augmecon.
set_runs_control( stop_on_infeasible = FALSE, stop_on_no_solution = FALSE, stop_on_error = TRUE )set_runs_control( stop_on_infeasible = FALSE, stop_on_no_solution = FALSE, stop_on_error = TRUE )
stop_on_infeasible |
Logical. If |
stop_on_no_solution |
Logical. If |
stop_on_error |
Logical. If |
Multi-objective methods commonly solve a sequence of related optimization models. Some parameter combinations, particularly restrictive epsilon levels, may be infeasible or may fail to produce a usable solution.
set_runs_control() determines whether such events stop the entire
multi-objective workflow or are recorded in the resulting
solutionset-class object.
A SolutionSet distinguishes between:
a run_id, which identifies every attempted optimization run;
a solution_id, which is assigned only when a run produces a
stored solution.
When a run is retained after an infeasibility or missing-solution event, its
run-level metadata remains available through get_runs, but its
solution_id and objective values will generally be missing.
Infeasible runs
If stop_on_infeasible = FALSE, an infeasible run is recorded and the
workflow continues with the remaining run design. This is generally useful
when exploring automatically generated epsilon grids because restrictive
combinations of epsilon levels may be infeasible.
If stop_on_infeasible = TRUE, the workflow stops when the first
infeasible run is encountered.
Runs without a solution vector
A solver may terminate without returning a usable decision vector even when the outcome is not classified explicitly as infeasible.
If stop_on_no_solution = FALSE, the attempted run is recorded without
a stored solution and the remaining runs are attempted. If
stop_on_no_solution = TRUE, the workflow stops immediately.
Unexpected errors
If stop_on_error = TRUE, unexpected errors raised while preparing,
solving, or processing a run are propagated and stop the workflow. This is
the recommended default because such errors may indicate an invalid model,
unsupported solver behaviour, or an internal implementation problem.
If stop_on_error = FALSE, the workflow attempts to record the failed
run and continue. This option should be used cautiously because it may conceal
modelling or implementation errors.
The default settings favour completing the requested run design while still stopping on unexpected errors:
stop_on_infeasible = FALSE stop_on_no_solution = FALSE stop_on_error = TRUE
An object of class RunsControl and MOControl containing
the validated execution-control settings. The object is intended to be
supplied to the control argument of a supported multi-objective
method.
set_method_epsilon_constraint,
set_method_augmecon,
set_runs_grid,
set_runs_manual,
get_runs,
solutionset-class
# Default behaviour: continue after infeasible runs or runs without a # solution, but stop on unexpected errors control <- set_runs_control() control # Stop as soon as an infeasible run or missing solution is encountered strict_control <- set_runs_control( stop_on_infeasible = TRUE, stop_on_no_solution = TRUE ) strict_control# Default behaviour: continue after infeasible runs or runs without a # solution, but stop on unexpected errors control <- set_runs_control() control # Stop as soon as an infeasible run or missing solution is encountered strict_control <- set_runs_control( stop_on_infeasible = TRUE, stop_on_no_solution = TRUE ) strict_control
Create an automatic run-design specification for generating multiple optimization runs in a multi-objective workflow.
set_runs_grid() provides a common interface for controlling the
resolution of weighted-sum, epsilon-constraint, and AUGMECON run designs.
The returned object does not contain the final run table. Instead, it stores
the requested grid resolution, which is resolved later by the corresponding
set_method_*() function using the registered objectives and their
optimization senses.
set_runs_grid(n)set_runs_grid(n)
n |
Integer. Resolution of the automatically generated run design.
Must be at least |
Multi-objective methods generally require several optimization runs to
explore different regions of objective space. set_runs_grid() asks
multiscape to generate those runs automatically from a grid.
The interpretation of the grid depends on the selected method:
In set_method_weighted_sum, the grid defines
combinations of objective weights. For two objectives, n gives the
number of weight combinations between the two pure-objective extremes. For
three or more objectives, n controls the resolution of the generated
simplex grid. The generated weights are normalized according to the method
settings and represent alternative preferences among the registered
objectives.
In set_method_epsilon_constraint, the grid defines
epsilon levels for the constrained objective. The primary objective is
optimized directly, while the remaining objective is progressively
restricted across the generated runs.
In set_method_augmecon, the grid defines epsilon
levels for the secondary objectives, which are then used in the augmented
epsilon-constraint formulation. When several secondary objectives are used,
the final run design is obtained by combining the generated epsilon levels
across objectives.
The argument n controls the resolution of the automatically generated
design. It should not always be interpreted as the final number of runs.
For example, when several secondary objectives are present, epsilon levels
may be combined across objectives and generate more runs than the value
supplied to n. Likewise, the number of valid weighted combinations
depends on the number of objectives and on how the weight grid is
constructed.
Boundary levels are always included. For weighted-sum methods, this means that pure-objective weight vectors are included, where all weight is assigned to one objective. For epsilon-constraint and AUGMECON methods, this means that the lower and upper bounds of the automatically derived epsilon ranges are included.
Including boundary levels helps recover the best observed value of each objective and provides reference points for subsequent frontier analyses.
The resolved design is stored in the resulting
solutionset-class object and can be inspected after solving
through get_runs.
Use set_runs_manual instead when exact weights or epsilon levels
must be supplied explicitly.
An object of class RunGrid and RunDesign. The object
stores the requested grid resolution and is intended to be supplied to the
runs argument of a multi-objective method function.
set_runs_manual,
set_method_weighted_sum,
set_method_epsilon_constraint,
set_method_augmecon,
get_runs
# Create an automatic run-grid specification grid <- set_runs_grid(n = 5) grid # Use the automatic grid with a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = grid, normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Inspect the resolved run design and objective values get_runs(solutions) get_objectives( solutions, format = "wide" ) }# Create an automatic run-grid specification grid <- set_runs_grid(n = 5) grid # Use the automatic grid with a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = grid, normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Inspect the resolved run design and objective values get_runs(solutions) get_objectives( solutions, format = "wide" ) }
Create an explicit run-design specification in which each row defines one multi-objective optimization run.
set_runs_manual() is used when exact objective weights or epsilon
levels should be supplied by the user instead of being generated
automatically with set_runs_grid.
set_runs_manual(x)set_runs_manual(x)
x |
A non-empty |
The input must be a non-empty data.frame with one row per requested
optimization run.
Weighted-sum columns must follow the convention:
weight_<alias>
Epsilon-constraint and AUGMECON columns must follow the convention:
eps_<alias>
A manual design must contain at least one column beginning with
weight_ or eps_. Mixing both column families in the same
design is not allowed.
All run-design columns must be numeric, finite, and free of missing values. Weight columns must contain non-negative values, and each weighted-sum row must assign a strictly positive total weight.
This function performs method-independent structural validation.
Method-specific validation is performed later by the corresponding
set_method_*() function. This includes checking:
that all required objective aliases are represented;
that no unknown objective columns are supplied;
that epsilon columns correspond only to secondary objectives;
and that the supplied design is compatible with the selected method.
Therefore, an object may be structurally valid for
set_runs_manual() but rejected later by
set_method_weighted_sum,
set_method_epsilon_constraint, or
set_method_augmecon.
An object of class RunManual and RunDesign containing
the validated run table.
set_runs_grid,
set_method_weighted_sum,
set_method_epsilon_constraint,
set_method_augmecon
weighted_runs <- set_runs_manual( data.frame( weight_cost = c(1.0, 0.5, 0.0), weight_benefit = c(0.0, 0.5, 1.0) ) ) epsilon_runs <- set_runs_manual( data.frame( eps_benefit = c(2, 4, 6, 8) ) ) weighted_runs epsilon_runsweighted_runs <- set_runs_manual( data.frame( weight_cost = c(1.0, 0.5, 0.0), weight_benefit = c(0.0, 0.5, 1.0) ) ) epsilon_runs <- set_runs_manual( data.frame( eps_benefit = c(2, 4, 6, 8) ) ) weighted_runs epsilon_runs
Store solver configuration inside a Problem object so that
solve can later run using the stored backend and runtime
options.
This function does not build or solve the optimization model. It only updates
the solver configuration stored in x$data$solve_args.
set_solver( x, solver = NULL, gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL, solver_params = list(), ... )set_solver( x, solver = NULL, gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL, solver_params = list(), ... )
x |
A |
solver |
Character string indicating the solver backend to use. Must be
one of |
gap_limit |
Optional numeric value in |
time_limit |
Optional non-negative numeric value giving the maximum
solving time in seconds. If |
solution_limit |
Optional logical flag requesting early termination
after a feasible solution is found. Supported by Gurobi, CBC, and
SYMPHONY, but not by CPLEX through Rcplex. If |
cores |
Optional positive integer giving the maximum number of solver
threads. Currently supported by Gurobi. If |
verbose |
Optional logical flag indicating whether the solver should
print log output. If |
log_file |
Optional character string giving the complete path or file
name of the solver log. Currently supported by Gurobi. If |
write_log |
Optional logical flag indicating whether solver output
should be written to a file. Currently supported by Gurobi. If
|
solver_params |
Named list of solver-specific parameters. These are merged with previously stored parameters. Rcplex parameters are validated against its supported control names; Rsymphony does not currently receive arbitrary solver-specific parameters. |
... |
Additional named solver-specific parameters. These are merged into
|
Purpose
The multiscape workflow separates problem specification from solver
configuration. Problem data, actions, effects, targets, objectives, and
methods are stored in the Problem object, and solver settings are
stored separately in x$data$solve_args.
This function allows solver options to be configured once and reused later
through solve(x) without repeating the same arguments each time.
Stored fields
The solver configuration is stored in x$data$solve_args. Typical
entries include:
solver,
gap_limit,
time_limit,
solution_limit,
cores,
verbose,
write_log,
log_file,
solver_params.
Single configuration
Solver settings can be configured only once per problem, including through
backend wrappers. A second call raises an error, even if the arguments are
identical. Supply all settings together. Arguments left as NULL use
solver defaults. Reading defaults does not count as explicit configuration.
To compare configurations, derive each alternative from the same problem
before the first solver call.
Gap limit
The argument gap_limit is interpreted as a relative optimality gap for
mixed-integer optimization. It must lie in .
If the solver stops with incumbent value and best
bound , then the exact stopping rule depends on the
solver backend, but conceptually gap_limit controls the maximum
accepted relative difference between the incumbent and the bound.
Time limit
The argument time_limit is interpreted as a maximum wall-clock time in
seconds allowed for the solver.
Solution limit
The argument solution_limit is stored as a logical flag. Its exact
meaning depends on the backend-specific solving layer, but conceptually it
requests early termination after finding a feasible solution according to the
behaviour supported by the chosen solver.
Cores
The argument cores specifies the maximum number of solver threads.
It is supported by Gurobi. Rcplex, rcbc, and Rsymphony do not reliably
expose thread control through the interfaces used here; multiscape therefore
warns and ignores cores for CPLEX, CBC, and SYMPHONY. If
the requested number exceeds the number of detected logical processors, it
is capped to the detected maximum with a warning.
Verbose output and log files
The arguments verbose, write_log, and log_file
control how solver logging is handled. These options are stored and later
interpreted by the solving layer for the selected backend. Solver log files
are currently available only with Gurobi. A parameter that is not available
through the selected R solver interface is reported with a warning and is
not stored as if it had been applied.
Backend capabilities
Common parameters are translated only when the selected backend supports them:
Gurobi supports cores, solution_limit, and
write_log.
CBC supports solution_limit. Thread control and CBC log
files are not exposed reliably through the current rcbc integration.
CPLEX through Rcplex does not expose cores,
solution_limit, or solver log files.
SYMPHONY through Rsymphony supports solution_limit, but does
not expose cores or solver log files.
Solver-specific parameters
Additional backend-specific parameters can be passed in two ways:
through the named list solver_params,
through additional named arguments in ....
These two sources are merged within the single configuration call.
This is particularly useful for backend-specific controls such as node selection, emphasis parameters, tolerances, or heuristics.
Supported backends
The solver argument selects the backend to be used later by
solve. Supported values are:
"auto": let the solving layer choose an available backend,
"gurobi",
"cplex",
"cbc",
"symphony".
This function only stores the requested backend. Availability of the backend is checked later when solving.
An updated Problem object with modified solver settings stored
in x$data$solve_args.
solve,
set_solver_gurobi,
set_solver_cplex,
set_solver_cbc,
set_solver_symphony
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x1 <- set_solver( x, solver = "cbc", gap_limit = 0.01, time_limit = 300, cores = 2, verbose = TRUE ) x1$data$solve_args # Define an alternative from the common unconfigured problem x2 <- set_solver( x, gap_limit = 0.05, solver_params = list(randomSeed = 123) ) x2$data$solve_args# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x1 <- set_solver( x, solver = "cbc", gap_limit = 0.01, time_limit = 300, cores = 2, verbose = TRUE ) x1$data$solve_args # Define an alternative from the common unconfigured problem x2 <- set_solver( x, gap_limit = 0.05, solver_params = list(randomSeed = 123) ) x2$data$solve_args
Convenience wrapper around set_solver that stores
solver = "cbc" in the problem object.
This function does not solve the model. It only updates the stored solver configuration. A solver can be configured only once per problem. Calling this wrapper after any solver setter raises an error. Derive alternatives from the same problem before its first solver configuration.
set_solver_cbc( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )set_solver_cbc( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )
x |
A |
... |
Additional named solver-specific parameters. These are merged into
|
solver_params |
Named list of solver-specific parameters. These are merged with previously stored parameters. Rcplex parameters are validated against its supported control names; Rsymphony does not currently receive arbitrary solver-specific parameters. |
gap_limit |
Optional numeric value in |
time_limit |
Optional non-negative numeric value giving the maximum
solving time in seconds. If |
solution_limit |
Optional logical flag requesting early termination
after a feasible solution is found. Supported by Gurobi, CBC, and
SYMPHONY, but not by CPLEX through Rcplex. If |
cores |
Optional positive integer giving the maximum number of solver
threads. Currently supported by Gurobi. If |
verbose |
Optional logical flag indicating whether the solver should
print log output. If |
log_file |
Optional character string giving the complete path or file
name of the solver log. Currently supported by Gurobi. If |
write_log |
Optional logical flag indicating whether solver output
should be written to a file. Currently supported by Gurobi. If
|
An updated Problem object with CBC solver settings stored in
x$data$solve_args.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_cbc( x, gap_limit = 0.01, time_limit = 300, cores = 2, solution_limit = FALSE ) x$data$solve_args# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_cbc( x, gap_limit = 0.01, time_limit = 300, cores = 2, solution_limit = FALSE ) x$data$solve_args
Convenience wrapper around set_solver that sets
solver = "cplex".
A solver can be configured only once per problem. Calling this wrapper after
any solver setter raises an error. Derive alternatives from the same problem
before its first solver configuration.
set_solver_cplex( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )set_solver_cplex( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )
x |
A |
... |
Additional named solver-specific parameters. These are merged into
|
solver_params |
Named list of solver-specific parameters. These are merged with previously stored parameters. Rcplex parameters are validated against its supported control names; Rsymphony does not currently receive arbitrary solver-specific parameters. |
gap_limit |
Optional numeric value in |
time_limit |
Optional non-negative numeric value giving the maximum
solving time in seconds. If |
solution_limit |
Optional logical flag requesting early termination
after a feasible solution is found. Supported by Gurobi, CBC, and
SYMPHONY, but not by CPLEX through Rcplex. If |
cores |
Optional positive integer giving the maximum number of solver
threads. Currently supported by Gurobi. If |
verbose |
Optional logical flag indicating whether the solver should
print log output. If |
log_file |
Optional character string giving the complete path or file
name of the solver log. Currently supported by Gurobi. If |
write_log |
Optional logical flag indicating whether solver output
should be written to a file. Currently supported by Gurobi. If
|
An updated Problem object with CPLEX solver settings.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_cplex( x, gap_limit = 0.001, time_limit = 1200 ) x$data$solve_args# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_cplex( x, gap_limit = 0.001, time_limit = 1200 ) x$data$solve_args
Convenience wrapper around set_solver that stores
solver = "gurobi" in the problem object.
This function does not solve the model. It only updates the stored solver configuration. A solver can be configured only once per problem. Calling this wrapper after any solver setter raises an error. Derive alternatives from the same problem before its first solver configuration.
set_solver_gurobi( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )set_solver_gurobi( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )
x |
A |
... |
Additional named solver-specific parameters. These are merged into
|
solver_params |
Named list of solver-specific parameters. These are merged with previously stored parameters. Rcplex parameters are validated against its supported control names; Rsymphony does not currently receive arbitrary solver-specific parameters. |
gap_limit |
Optional numeric value in |
time_limit |
Optional non-negative numeric value giving the maximum
solving time in seconds. If |
solution_limit |
Optional logical flag requesting early termination
after a feasible solution is found. Supported by Gurobi, CBC, and
SYMPHONY, but not by CPLEX through Rcplex. If |
cores |
Optional positive integer giving the maximum number of solver
threads. Currently supported by Gurobi. If |
verbose |
Optional logical flag indicating whether the solver should
print log output. If |
log_file |
Optional character string giving the complete path or file
name of the solver log. Currently supported by Gurobi. If |
write_log |
Optional logical flag indicating whether solver output
should be written to a file. Currently supported by Gurobi. If
|
An updated Problem object with Gurobi solver settings stored
in x$data$solve_args.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_gurobi( x, gap_limit = 0.01, time_limit = 600, cores = 2, MIPFocus = 1 ) x$data$solve_args# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_gurobi( x, gap_limit = 0.01, time_limit = 600, cores = 2, MIPFocus = 1 ) x$data$solve_args
Convenience wrapper around set_solver that stores
solver = "symphony" in the problem object.
This function does not solve the model. It only updates the stored solver configuration. A solver can be configured only once per problem. Calling this wrapper after any solver setter raises an error. Derive alternatives from the same problem before its first solver configuration.
set_solver_symphony( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )set_solver_symphony( x, ..., solver_params = list(), gap_limit = NULL, time_limit = NULL, solution_limit = NULL, cores = NULL, verbose = NULL, log_file = NULL, write_log = NULL )
x |
A |
... |
Additional named solver-specific parameters. These are merged into
|
solver_params |
Named list of solver-specific parameters. These are merged with previously stored parameters. Rcplex parameters are validated against its supported control names; Rsymphony does not currently receive arbitrary solver-specific parameters. |
gap_limit |
Optional numeric value in |
time_limit |
Optional non-negative numeric value giving the maximum
solving time in seconds. If |
solution_limit |
Optional logical flag requesting early termination
after a feasible solution is found. Supported by Gurobi, CBC, and
SYMPHONY, but not by CPLEX through Rcplex. If |
cores |
Optional positive integer giving the maximum number of solver
threads. Currently supported by Gurobi. If |
verbose |
Optional logical flag indicating whether the solver should
print log output. If |
log_file |
Optional character string giving the complete path or file
name of the solver log. Currently supported by Gurobi. If |
write_log |
Optional logical flag indicating whether solver output
should be written to a file. Currently supported by Gurobi. If
|
An updated Problem object with SYMPHONY solver settings stored
in x$data$solve_args.
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_symphony( x, gap_limit = 0.05, time_limit = 300 ) x$data$solve_args# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- create_problem( pu = example_data$planning_units, features = example_data$features, dist_features = example_data$dist_features, cost = "cost" ) x <- set_solver_symphony( x, gap_limit = 0.05, time_limit = 300 ) x$data$solve_args
Historical Meseta-related distribution with 348,021 rows, paired with
sim_pu and sim_features. It is not the zero credited comparison
reference used in the introductory tutorial. Prefer load_meseta().
data(sim_dist_features)data(sim_dist_features)
A data frame linking planning units and features with an amount column.
Historical Meseta-related catalogue of 155 features, retained for
compatibility. These do not belong to the compact simulated example.
Prefer load_meseta() for the complete tutorial inputs.
data(sim_features)data(sim_features)
A data frame with feature identifiers and names.
Prefer load_sim_multiaction() as the entry point to this dataset.
Direct data() access remains available for compatibility.
data(sim_multiaction)data(sim_multiaction)
A named list with seven components:
planning_unitsAn sf object with 64 square planning units.
featuresA data frame with two feature identifiers and names.
dist_featuresA data frame of feature amounts by planning unit.
actionsA data frame describing protection and restoration.
action_costsA data frame of spatially varying action costs.
effect_assumptionsRelative changes by action and feature,
ready to pass to add_effects.
effectsExplicit signed changes in the historical delta
column, retained for compatibility. New examples use effect_assumptions.
A compact example dataset containing all inputs needed to build a multi-objective spatial planning problem with protection and restoration as mutually exclusive candidate actions.
Historical Meseta-related planning units: 11,109 spatial cells with unit
cost 1. These are distinct from sim_pu_sf and are not the complete
Meseta tutorial inputs. Prefer load_meseta() for that tutorial.
data(sim_pu)data(sim_pu)
An object of class sf.
Historical ecosystem-services planning units: 30,496 spatial cells.
These are distinct from sim_pu. Prefer load_ecosystem_services()
to load the matching planning units and feature raster.
data(sim_pu_sf)data(sim_pu_sf)
An object of class sf.
Append the runs and stored solutions from one
solutionset-class object to another.
This function combines two SolutionSet objects generated from the
same planning problem. It is intended for combining results obtained with
different multiscape solving workflows, such as weighted-sum,
epsilon-constraint, or AUGMECON methods, while keeping a single coherent
result object for downstream extraction, plotting, and analysis.
solution_append(x, y)solution_append(x, y)
x |
A |
y |
A second |
solution_append() is a solution-set management function. It does not
modify the original input objects. Instead, it returns a new
SolutionSet containing the runs and solutions from both inputs.
Both input objects must be generated from the same planning problem. This is checked conservatively before appending. In particular, the two objects must have compatible:
planning units;
features and feature distributions;
actions, feasible action pairs, and effects;
profit data, when present;
targets;
locks and constraints;
spatial relations;
objective specifications.
Differences in method settings, run design, solver settings, solver status, runtime, gaps, and other solve diagnostics are allowed.
The appended runs and solutions are assigned new run_id and
solution_id values to keep identifiers unique in the combined object.
Identifiers are not required to match between the two inputs.
This function is not intended to combine results from different planning
problems, scenarios, target sets, or objective definitions. Such workflows
should be handled by a future comparison/binding function rather than by
solution_append().
A new solutionset-class object containing the runs and
stored solutions from both input objects.
solution_filter,
solutionset-class,
get_runs,
get_objectives,
plot_tradeoff
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() make_problem <- function() { 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") } weighted_problem <- make_problem() |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid( n = 4 ), normalize_weights = TRUE ) epsilon_problem <- make_problem() |> set_method_epsilon_constraint( primary = "cost", runs = set_runs_grid( n = 4 ) ) if (requireNamespace("rcbc", quietly = TRUE)) { weighted_problem <- set_solver_cbc( weighted_problem, verbose = FALSE ) epsilon_problem <- set_solver_cbc( epsilon_problem, verbose = FALSE ) weighted_solutions <- solve(weighted_problem) epsilon_solutions <- solve(epsilon_problem) combined_solutions <- solution_append( weighted_solutions, epsilon_solutions ) # Inspect the combined run history get_runs(combined_solutions) # Inspect objective values from both methods get_objectives( combined_solutions, format = "wide" ) # The original objects remain unchanged get_runs(weighted_solutions) get_runs(epsilon_solutions) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() make_problem <- function() { 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") } weighted_problem <- make_problem() |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid( n = 4 ), normalize_weights = TRUE ) epsilon_problem <- make_problem() |> set_method_epsilon_constraint( primary = "cost", runs = set_runs_grid( n = 4 ) ) if (requireNamespace("rcbc", quietly = TRUE)) { weighted_problem <- set_solver_cbc( weighted_problem, verbose = FALSE ) epsilon_problem <- set_solver_cbc( epsilon_problem, verbose = FALSE ) weighted_solutions <- solve(weighted_problem) epsilon_solutions <- solve(epsilon_problem) combined_solutions <- solution_append( weighted_solutions, epsilon_solutions ) # Inspect the combined run history get_runs(combined_solutions) # Inspect objective values from both methods get_objectives( combined_solutions, format = "wide" ) # The original objects remain unchanged get_runs(weighted_solutions) get_runs(epsilon_solutions) }
Return a reduced solutionset-class object containing only the
runs or solutions that match the requested filters.
This function is intended to curate SolutionSet objects before
downstream analysis, plotting, frontier analysis, or post-hoc evaluation.
It filters all relevant components of the object consistently, including the
run table, design table, stored run-level solutions, and available summary
tables.
solution_filter( x, run_id = NULL, solution_id = NULL, status = NULL, feasible_only = FALSE, nondominated = FALSE, objectives = NULL )solution_filter( x, run_id = NULL, solution_id = NULL, status = NULL, feasible_only = FALSE, nondominated = FALSE, objectives = NULL )
x |
A |
run_id |
Optional integer vector of run ids to keep. |
solution_id |
Optional numeric vector of positive integer solution ids to keep. Runs without a stored solution are never matched by this filter. |
status |
Optional character vector of run statuses to keep. Matching is case-insensitive. |
feasible_only |
Logical. If |
nondominated |
Logical. If |
objectives |
Optional character vector of objective names to use when
|
A SolutionSet distinguishes between runs and stored solutions.
run_id identifies a run or attempted solve. Runs may be
feasible, optimal, infeasible, failed, or otherwise incomplete.
solution_id identifies a stored solution. Runs that did not
produce a solution have solution_id = NA.
Therefore, filtering by run_id and filtering by solution_id
are not always equivalent. For example, an infeasible run may have a
run_id but no solution_id.
The function filters:
x$solution$runs, using the selected run_ids;
x$solution$design, when it contains a run_id column;
x$solution$solutions, using the selected solution_ids;
all tables in x$summary that contain a run_id column.
The function does not renumber run_id or solution_id. This
preserves traceability to the original run design.
If more than one filter is supplied, filters are combined using logical
and. For example, setting both status = "optimal" and
solution_id = c(1, 3) keeps only optimal runs whose
solution_id is either 1 or 3.
If nondominated = TRUE, the function further keeps only non-dominated
solutions among the runs retained by the previous filters. Dominance is
evaluated in objective space using the objective values stored in the run
table. Objective senses are obtained from the objective specifications stored in
the original problem.
Non-dominated filtering requires the moocore package.
A filtered solutionset-class object.
solutionset-class,
solve,
get_runs,
get_objectives,
get_planning_units,
get_actions
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) runs <- get_runs(solutions) # Keep only runs with a usable solver status feasible_solutions <- solution_filter( solutions, feasible_only = TRUE ) # Keep selected runs selected_runs <- solution_filter( solutions, run_id = runs$run_id[1:2] ) # Keep one stored solution solution_ids <- runs$solution_id[ !is.na(runs$solution_id) ] if (length(solution_ids) > 0L) { selected_solution <- solution_filter( solutions, solution_id = solution_ids[1] ) } # Keep only optimal runs if ("optimal" %in% tolower(runs$status)) { optimal_solutions <- solution_filter( solutions, status = "optimal" ) } # Keep only non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) # Evaluate dominance using selected objectives nondominated_subset <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE, objectives = c("cost", "benefit") ) } }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) runs <- get_runs(solutions) # Keep only runs with a usable solver status feasible_solutions <- solution_filter( solutions, feasible_only = TRUE ) # Keep selected runs selected_runs <- solution_filter( solutions, run_id = runs$run_id[1:2] ) # Keep one stored solution solution_ids <- runs$solution_id[ !is.na(runs$solution_id) ] if (length(solution_ids) > 0L) { selected_solution <- solution_filter( solutions, solution_id = solution_ids[1] ) } # Keep only optimal runs if ("optimal" %in% tolower(runs$status)) { optimal_solutions <- solution_filter( solutions, status = "optimal" ) } # Keep only non-dominated solutions if (requireNamespace("moocore", quietly = TRUE)) { nondominated_solutions <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE ) # Evaluate dominance using selected objectives nondominated_subset <- solution_filter( solutions, feasible_only = TRUE, nondominated = TRUE, objectives = c("cost", "benefit") ) } }
Return a reduced solutionset-class object containing one
representative from each group of equivalent solutions.
Solutions can be considered equivalent according to either their decision vectors or their objective values.
solution_unique( x, by = c("decisions", "objectives"), keep = c("first", "last"), objectives = NULL, tolerance = sqrt(.Machine$double.eps) )solution_unique( x, by = c("decisions", "objectives"), keep = c("first", "last"), objectives = NULL, tolerance = sqrt(.Machine$double.eps) )
x |
A |
by |
Character. Definition of uniqueness. One of
|
keep |
Character. Which representative to retain from each group of
equivalent solutions. If |
objectives |
Optional character vector of objective names to compare
when |
tolerance |
Non-negative numeric tolerance used when comparing
objective values. It is only used when |
solution_unique() is a solution-set management function. It removes
repeated solutions while consistently filtering the run table, design table,
stored run-level solutions, and summary tables.
Two definitions of uniqueness are supported:
by = "decisions" compares the complete stored decision vector
of each solution. Two solutions are considered equivalent when their
decision vectors are identical. This identifies repeated planning-unit or
planning-unit/action configurations, even when they were generated by
different runs or multi-objective parameter combinations.
by = "objectives" compares the selected objective values.
Two solutions are considered equivalent when all compared objective values
are equal within the specified numerical tolerance. Such solutions
may still have different decision vectors.
Consequently, uniqueness in objective space and uniqueness in decision space are not equivalent. Two spatially different solutions may produce the same objective values, while repeated runs may generate exactly the same decision vector.
Only runs with a stored solution_id can be assessed. Runs without a
stored solution, such as infeasible runs, are preserved unchanged. This
retains the full run history while removing only duplicated stored
solutions.
The function does not renumber run_id or solution_id. The
representative retained from each duplicate group keeps its original
identifiers, preserving traceability to the original run design.
For by = "objectives", numerical equality is assessed using a
relative comparison:
where is specified by tolerance. This avoids treating
insignificant floating-point differences as distinct objective points.
A new solutionset-class object containing one
representative from each group of equivalent stored solutions, together
with any runs that did not produce a stored solution.
solution_filter,
solution_append,
get_objectives,
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Keep one representative for each distinct decision vector unique_decisions <- solution_unique( solutions, by = "decisions" ) # Keep one representative for each distinct objective point unique_objectives <- solution_unique( solutions, by = "objectives" ) # Compare only selected objectives unique_cost_benefit <- solution_unique( solutions, by = "objectives", objectives = c("cost", "benefit") ) # Keep the last representative of each duplicate group unique_last <- solution_unique( solutions, by = "decisions", keep = "last" ) # Compare the number of stored solutions sum(!is.na(get_runs(solutions)$solution_id)) sum(!is.na(get_runs(unique_decisions)$solution_id)) sum(!is.na(get_runs(unique_objectives)$solution_id)) # Typical cleaning workflow if (requireNamespace("moocore", quietly = TRUE)) { clean_solutions <- solutions |> solution_filter( feasible_only = TRUE, nondominated = TRUE ) |> solution_unique( by = "decisions" ) } }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() problem <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) |> add_objective_max_effect(alias = "benefit") |> set_method_weighted_sum( aliases = c("cost", "benefit"), runs = set_runs_grid(n = 3), normalize_weights = TRUE ) if (requireNamespace("rcbc", quietly = TRUE)) { problem <- set_solver_cbc( problem, verbose = FALSE ) solutions <- solve(problem) # Keep one representative for each distinct decision vector unique_decisions <- solution_unique( solutions, by = "decisions" ) # Keep one representative for each distinct objective point unique_objectives <- solution_unique( solutions, by = "objectives" ) # Compare only selected objectives unique_cost_benefit <- solution_unique( solutions, by = "objectives", objectives = c("cost", "benefit") ) # Keep the last representative of each duplicate group unique_last <- solution_unique( solutions, by = "decisions", keep = "last" ) # Compare the number of stored solutions sum(!is.na(get_runs(solutions)$solution_id)) sum(!is.na(get_runs(unique_decisions)$solution_id)) sum(!is.na(get_runs(unique_objectives)$solution_id)) # Typical cleaning workflow if (requireNamespace("moocore", quietly = TRUE)) { clean_solutions <- solutions |> solution_filter( feasible_only = TRUE, nondominated = TRUE ) |> solution_unique( by = "decisions" ) } }
The SolutionSet class stores the result of solving a
Problem object.
A SolutionSet object is the standard result object returned by
solve. It may contain a single solved run, as in ordinary
single-objective optimization, or multiple solved runs, as in weighted-sum,
-constraint, or AUGMECON workflows.
The object stores the original problem, run-level design and outcome tables, objective values, diagnostics, method metadata, and additional metadata needed to inspect, compare, and analyse optimization results.
Conceptual role
The SolutionSet class represents the result of a solving workflow:
Problem -> solve() -> SolutionSet
Single-objective workflows are represented as SolutionSet objects with
one run. Multi-objective workflows are represented as SolutionSet
objects with one or more runs generated by the selected method.
This design provides a common result structure for downstream analysis, including objective-value extraction, run diagnostics, trade-off analysis, Pareto-frontier summaries, distance-based ranking, and comparison between scenarios.
Typical use cases
A SolutionSet is used to inspect and analyse the results returned by
solve. Typical use cases include:
inspecting selected planning units or actions;
extracting run-level objective values;
comparing alternative solutions produced by a multi-objective method;
analysing trade-offs between objectives;
filtering feasible, unique, or non-dominated solutions;
computing distances to observed ideal or nadir points;
preparing plots and tables for reporting.
Internal structure
A SolutionSet object separates information into several layers:
problemThe Problem object used to generate the
solution set.
solutionA named list containing the core solving
outputs. This typically includes the run design, the run summary table, and
the stored run-level solution objects used internally by multiscape.
summaryA named list containing user-facing summaries
aggregated across runs when such summaries are available.
diagnosticsA named list containing diagnostics about
the solution set as a whole.
methodA named list describing the optimization method
used.
metaA named list containing additional metadata.
nameA character(1) identifier for the solution set.
Run-level content
The solution field is the main internal container for run-level
information. Typical entries include:
designA data.frame describing the optimization
design. In multi-objective workflows this may include weights,
-levels, or other method-specific parameters. In
single-objective workflows this table usually contains one row.
runsA data.frame summarizing the outcome of each run.
This typically includes run identifiers, solver status, runtime, optimality
gap, objective values, and method-specific information.
solutionsA list of stored run-level solution objects used internally by multiscape. Users should normally rely on accessor functions rather than directly manipulating this list.
In most workflows, the runs table is the most useful compact
representation of the solution set. Detailed inspection should preferably be
done through accessor functions such as get_pu,
get_actions, get_features, and
get_targets.
Objective values and run tables
The run table may store objective values in columns named
value_<alias>, where <alias> is the alias of a registered
objective. For example:
value_cost,
value_frag,
value_benefit.
This convention provides a compact format for downstream functions that analyse trade-offs, frontiers, or distances between solutions.
Depending on the solving method, the run table may also contain design columns such as:
weight_<alias> for weighted-sum runs;
eps_<alias> for -constraint or AUGMECON runs.
Single-objective and multi-objective results
solve() always returns a SolutionSet. For a single-objective
problem, the returned object contains one run. For a multi-objective
workflow, the returned object contains one or more runs generated by the
selected method.
This unified output structure makes it possible to use the same inspection and analysis functions regardless of whether the original problem was single-objective or multi-objective.
Diagnostics
The diagnostics field stores metadata about the solve process.
Depending on the workflow, it may summarize:
number of design rows;
number of attempted runs;
number of stored solutions;
status frequencies;
runtime ranges;
gap ranges;
and other aggregate information about the set of runs.
Printing
The print() method provides a concise summary of the solution set.
It reports:
the optimization method and participating objective aliases;
the run-design type when this information is available;
the number of design rows, attempted runs, stored solutions, and runs without a stored solution;
run-level status, runtime, and optimality-gap summaries;
the observed range of each objective across stored solutions;
and any filtering, uniqueness, or append metadata already recorded in the object.
Printing does not calculate non-dominance, uniqueness, similarities, selection frequencies, or distances. These analyses are intentionally kept outside the print method so that printing remains fast and does not depend on optional packages.
This printed output is intended as a quick overview. Detailed inspection
should use public accessor and analysis functions such as
get_runs, get_objectives,
solution_filter, and
frontier_distances.
No return value. This page documents the SolutionSet class.
problemThe Problem object used to generate the
solution set.
solutionA named list containing the core solving
outputs, typically including design, runs, and internally
stored run-level solutions.
summaryA named list containing user-facing summaries
associated with the solution set.
diagnosticsA named list containing diagnostics about
the solution set as a whole.
methodA named list describing the optimization method
used.
metaA named list of additional metadata.
nameA character(1) name for the solution set
object.
print()Print a concise summary of the solution set, including method name, number of runs, and run-level diagnostics.
show()Alias of print().
repr()Return a short one-line representation of the solution set.
getMethod()Return the method specification stored in
self$method.
getDesign()Return the design table stored in
self$solution$design.
getRuns()Return the run summary table stored in
self$solution$runs.
getSolutions()Return the internally stored run-level solution objects. This is mainly intended for advanced use; most users should prefer higher-level accessor functions.
solve,
plot_tradeoff,
get_pu,
get_actions,
get_features,
get_targets
Solve a planning problem stored in a Problem object.
This is the main execution step of the multiscape workflow. It reads
the problem specification stored in a Problem object, builds the
corresponding optimization model when needed, applies the configured solver
settings, and returns a solutionset-class object.
A SolutionSet is the standard result object returned by
solve(). Single-objective workflows are represented as one-run
SolutionSet objects, while multi-objective workflows are represented
as multi-run SolutionSet objects.
solve(x, ...) ## S3 method for class 'Problem' solve(x, ...)solve(x, ...) ## S3 method for class 'Problem' solve(x, ...)
x |
A |
... |
Additional arguments reserved for internal or legacy solver handling. These are not part of the main recommended user interface. |
Role of solve()
The typical multiscape workflow is:
x <- create_problem(...) x <- add_...(x, ...) x <- set_...(x, ...) res <- solve(x)
Thus, solve() is the stage at which the stored problem specification
is turned into one or more optimization runs.
For most users, solve() is the standard execution entry point.
Explicit compilation with compile_model is optional and is
mainly useful for advanced inspection or debugging workflows.
What solve() uses from the problem object
The function uses the information already stored in the Problem
object, including:
baseline planning data;
actions, effects, profit, and spatial relations;
targets and constraints;
registered objectives;
an optional multi-objective method configuration;
solver settings.
If a model has not yet been built, it is built internally during the solve process. If a model snapshot or pointer already exists, the solving layer may reuse or refresh it depending on the internal model state.
Single-objective and multi-objective behaviour
solve() always returns a solutionset-class object.
Single-objective case. If exactly one objective is active
and no multi-objective method is configured, solve() runs a single
optimization problem and returns a one-run SolutionSet.
Multi-objective case. If a multi-objective method is
configured, solve() dispatches internally according to the stored
method name and returns a multi-run SolutionSet.
This unified output structure makes it possible to use the same inspection, plotting, and analysis functions regardless of whether the original problem was single-objective or multi-objective.
Currently supported multi-objective method names are:
"weighted";
"epsilon_constraint";
"augmecon".
Consistency rule
If multiple objectives are registered but no multi-objective method has been
selected, solve() stops with an error. In practical terms:
one objective and no multi-objective method
single-objective solve;
multiple objectives and a valid multi-objective method
multi-objective solve;
multiple objectives and no multi-objective method
error.
Implicit conservation-planning model
If no explicit actions and no explicit effects are provided, solve()
can build the corresponding classical conservation-planning formulation
internally. In this case, selecting a planning unit is interpreted as applying
an implicit conservation action, and the baseline feature amounts stored in
dist_features count toward representation targets.
This allows standard reserve-selection problems to be solved without
requiring users to manually define actions and effects. Explicit
action-based workflows remain available by using add_actions
and add_effects.
Solver settings
Solver configuration is read from the Problem object, typically after
calling set_solver or one of its convenience wrappers such as
set_solver_gurobi or set_solver_cbc.
These settings may include:
the selected backend;
time limits;
optimality-gap settings;
CPU cores;
verbosity options;
backend-specific solver parameters.
Method dispatch
solve() is an S3 generic. The public method documented here is
solve.Problem(), which operates on Problem objects.
A solutionset-class object.
The returned object contains run-level information, solver diagnostics,
objective values, and stored optimization outputs. For single-objective
problems, the returned SolutionSet contains one run. For
multi-objective workflows, it contains one or more runs generated by the
selected method.
Users will typically inspect or visualize results using accessor and plotting
functions such as get_pu, get_actions,
get_features, get_targets, and
plot_tradeoff.
problem-class,
solutionset-class,
compile_model,
set_solver,
set_solver_cbc,
set_solver_gurobi,
set_method_weighted_sum,
set_method_epsilon_constraint,
set_method_augmecon,
add_actions,
add_effects
# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) if (requireNamespace("rcbc", quietly = TRUE)) { x <- set_solver_cbc(x, verbose = FALSE) solset <- solve(x) print(solset) }# Load a complete simulated planning problem. example_data <- load_sim_multiaction() x <- 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 ) |> add_effects( example_data$effect_assumptions ) |> add_constraint_targets_relative(0.05) |> add_objective_min_cost(alias = "cost", include_pu_cost = FALSE) if (requireNamespace("rcbc", quietly = TRUE)) { x <- set_solver_cbc(x, verbose = FALSE) solset <- solve(x) print(solset) }