The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

multiscape is an exact optimisation framework for
multi-objective spatial planning in R. It is designed
for planning problems in which spatial data, ecological or socioeconomic
features, constraints, and multiple competing objectives must be
considered simultaneously within a single decision-support workflow. The
package is built around mixed-integer linear programming
(MILP) formulations, allowing users to represent spatial
planning problems explicitly as optimisation models and solve them with
exact methods. This makes multiscape especially suitable
for applications where transparent model structure, reproducibility, and
rigorous trade-off analysis are important. multiscape
supports both general spatial planning formulations and action-based
formulations in which decisions are expressed as management
actions applied across planning units. With it, users can build
planning problems from tabular or spatial inputs, define feasible
actions and their effects, add targets and other constraints, register
multiple objectives such as cost, benefit, profit, or fragmentation, and
explore exact trade-offs using multi-objective methods such as
weighted-sum, epsilon-constraint, and AUGMECON.
You can install the stable version of multiscape from
CRAN:
install.packages("multiscape")And, the current development version can be installed from GitHub:
if (!requireNamespace("remotes", quietly = TRUE)) {
install.packages("remotes")
}
remotes::install_github("josesalgr/multiscape")multiscape?multiscape brings together several key components of
spatial planning in a single optimisation framework:
A typical multiscape workflow has five steps.
First, the user creates a Problem object from planning
units, features, and baseline feature amounts. Second, the user adds
feasible actions and, when relevant, defines how those actions affect
the features of interest. Third, the user adds targets, constraints, and
spatial relations. Fourth, the user registers one or more atomic
objectives. Finally, the user solves the problem in single-objective or
multi-objective mode.
In other words, multiscape separates problem definition
from optimization. The Problem object stores the planning
specification, and the solving stage later translates that specification
into one or more exact optimization runs.
multiscape currently supports several exact
multi-objective workflows.
The weighted-sum method combines multiple registered objectives into a single scalar objective through user-defined weights. It is simple and useful for preference-driven exploration of trade-offs.
The epsilon-constraint method optimizes one objective directly while transforming the remaining objectives into explicit performance constraints. This is especially useful when one objective should be prioritised while the others are controlled through thresholds.
The AUGMECON method extends epsilon-constraint with an augmented formulation designed to reduce weakly efficient solutions and improve Pareto-front generation.
Together, these methods allow users to move beyond a single objective solution and instead analyse sets of efficient trade-off solutions.
The example below illustrates a simple multi-objective spatial action planning problem. Some planning units can only be managed through conservation, whereas others can only be managed through restoration. The aim is to identify efficient trade-offs between total cost and impact on ecosystem services while satisfying a minimum area requirement.
library(multiscape)
library(dplyr)
library(terra)
data("sim_pu_sf", package = "multiscape")
sim_features <- load_sim_features_raster()
n_pu <- nrow(sim_pu_sf)
area_pu <- sim_pu_sf$area[1] This example considers a landscape with 30,496 planning units and
four ecosystem-service layers represented in sim_features.
For each unit, the model can assign one of two management actions,
conservation or restoration, whenever that
action is feasible. The goal is to explore the trade-off between
minimizing total implementation cost and minimizing impact on the
baseline ecosystem services. Instead of returning a single solution, the
workflow uses an epsilon-constraint method to generate several efficient
alternatives that reveal the cost-impact trade-off.
These four raster layers represent the baseline ecosystem-service conditions used by the optimisation model. They define the spatial distribution of the features that are later affected by conservation and restoration actions.
terra::plot(
sim_features,
main = c(
"Rodent and lagomorph control",
"Carbon stock",
"Seasonal water yield",
"Sediment delivery ratio"
)
)
We first create the base Problem object from the
planning units and the feature layers.
p <- create_problem(
pu = sim_pu_sf,
features = sim_features,
cost = "cost"
)
p
#> A multiscape object (<Problem>)
#> ├─data
#> │├─planning units: <data.frame> (30496 total)
#> │├─costs: min: 0, max: 779.43
#> │└─features: 4 total ("L1_control_inv", "L1_stock_inv", "L1_rend_inv", ...)
#> └─actions and effects
#> │├─actions: none
#> │├─feasible action pairs: none
#> │├─effect data: none
#> │└─profit data: none
#> └─spatial
#> │├─geometry: sf (30496 rows)
#> │├─coordinates: 30496 rows (x: 344825.53946..350775.13398, y:
#> 6225654.6982..6229659.6982)
#> │└─relations: none
#> └─targets and constraints
#> │├─targets: none
#> │├─area constraints: none
#> │├─budget constraints: none
#> │├─planning-unit locks: none
#> │└─action locks: none
#> └─model
#> │├─status: not built yet (will build in solve())
#> │├─objectives: none
#> │├─method: single-objective
#> │├─solver: not set (auto)
#> │└─checks: incomplete (no objective registered)
#> # ℹ Use `x$data` to inspect stored tables and model snapshots.This call defines the spatial domain of the optimisation problem and
tells multiscape which planning-unit attribute should be
interpreted as cost. Importantly, this is still only a problem
definition. The model does not yet know which actions are
available, where they are admissible, how they affect the features, or
which objectives should be optimised. Those elements are added
explicitly in the following sections.
Printing the Problem object at this stage is useful
because it provides a compact summary of the current planning
specification. It lets the user verify that the planning units,
features, and baseline inputs have been correctly registered before
actions, effects, constraints, and objectives are added.
We now specify the catalogue of actions that the model is allowed to assign.
actions_df <- data.frame(
id = c("conservation", "restoration"),
name = c("conservation", "restoration")
)Here, the action set is deliberately simple and includes only two interventions: conservation and restoration. In more complex applications, the same structure can be extended to represent larger management portfolios, alternative treatment intensities, or multiple intervention types.
Not every action is allowed in every planning unit. In this example,
the variable locked_in is used to derive a simple
action-eligibility rule for the illustrative dataset: one subset of
planning units is only eligible for conservation,
whereas the complementary subset is only eligible for
restoration.
feasible_forest <- sim_pu_sf |>
sf::st_drop_geometry() |>
select(id, locked_in) |>
filter(locked_in) |>
transmute(pu = id, action = "conservation")
feasible_agro <- sim_pu_sf |>
sf::st_drop_geometry() |>
select(id, locked_in) |>
filter(!locked_in) |>
transmute(pu = id, action = "restoration")
include_df <- bind_rows(feasible_forest, feasible_agro) |>
distinct()The resulting table defines the feasible planning unit-action combinations that the optimizer is allowed to use. This is an important aspect of the action-based formulation: feasibility is not inferred automatically by the solver, but is instead encoded directly by the user as part of the planning specification. In other words, the optimisation model can only allocate actions where they have been declared admissible.
We next specify how actions affect the features.
effects_mult <- data.frame(
action = rep(c("conservation", "restoration"), each = 4),
feature = rep(
c("L1_control_inv", "L1_stock_inv", "L1_rend_inv", "L1_reten_inv"),
times = 2
),
multiplier = 1
)This table assigns an effect to every action × feature
combination. Because effect_type = "delta" will be used
later, the values are interpreted as action-induced
changes relative to the baseline feature representation. In the
present example, the same multiplier (1) is assigned to all
combinations, so the ecological response is intentionally kept simple:
both conservation and restoration are assumed to contribute with the
same relative magnitude across all four features.
This means that, in this README example, the difference between the two actions comes mainly from where they are eligible and how much they cost, rather than from different ecological effect strengths. In a more realistic application, one would often use different multipliers by action and feature, for example making restoration stronger for some features and weaker, neutral, or even negative for others.
Costs are attached to planning unit-action combinations so that the model can compare interventions not only by their ecological consequences, but also by their local implementation burden.
action_cost_df <- sim_pu_sf |>
sf::st_drop_geometry() |>
transmute(
pu = id,
action = ifelse(locked_in, "conservation", "restoration"),
cost = cost
) |>
semi_join(include_df, by = c("pu", "action"))This representation is useful in action-based planning because the
same planning unit may imply different costs depending on the
intervention that is permitted or selected. By storing costs at the
planning unit × action level, the optimisation problem can
evaluate trade-offs in a more realistic way than with a single uniform
planning-unit cost.
Once actions, eligibility, effects, and costs have been defined, we can complete the optimisation model.
target_area <- n_pu * area_pu * 0.20
p <- p |>
add_actions(actions = actions_df,
cost = action_cost_df,
include = include_df) |>
add_effects(effects = effects_mult,
effect_type = "delta") |>
add_objective_min_cost(alias = "Total cost",
include_pu_cost = FALSE,
include_action_cost = TRUE) |>
add_objective_min_intervention_impact(alias = "Total impact") |>
add_constraint_targets_relative(0) |>
add_constraint_area(area = target_area,
sense = "min") |>
add_constraint_locked_pu(locked_in = "locked_in",
locked_out = "locked_out") The call to add_actions() links the action catalogue to
the admissible allocation table and the action-specific cost table. The
call to add_effects() then tells the model how these
actions modify the tracked features. After that, the optimisation
problem is completed by combining constraints and objectives.
The minimum-area constraint imposes a lower bound on the amount of land to be selected, ensuring that the solution represents a non-trivial intervention effort. In this example, the area threshold is expressed in area units rather than as a simple count of planning units, which is why the code multiplies the target number of selected units by the area represented by one planning unit in the example dataset. The registered objectives define the trade-off of interest: one objective minimizes total action cost, while the other minimizes intervention impact. The epsilon-constraint method is then used to generate a set of efficient alternatives rather than a single solution.
p <- p |>
set_method_epsilon_constraint(
primary = "Total impact",
n_points = 4,
mode = "auto",
lexicographic = TRUE
) |>
set_solver_cbc(gap_limit = 0.1)
p
#> A multiscape object (<Problem>)
#> ├─data
#> │├─planning units: <data.frame> (30496 total)
#> │├─costs: min: 0, max: 779.43
#> │└─features: 4 total ("L1_control_inv", "L1_stock_inv", "L1_rend_inv", ...)
#> └─actions and effects
#> │├─actions: 2 total ("conservation", "restoration")
#> │├─feasible action pairs: 30496 feasible rows
#> │├─action costs: min: 0, max: 779.43
#> │├─effect data: 105537 rows
#> │├─effect mode: benefit only
#> │└─profit data: none
#> └─spatial
#> │├─geometry: sf (30496 rows)
#> │├─coordinates: 30496 rows (x: 344825.53946..350775.13398, y:
#> 6225654.6982..6229659.6982)
#> │└─relations: none
#> └─targets and constraints
#> │├─targets: 4 rows
#> │├─target preview: "L1_control_inv" >= 0, "L1_stock_inv" >= 0, "L1_rend_inv" >=
#> 0
#> │├─area constraints: 1 ([all] min)
#> │├─budget constraints: none
#> │├─planning-unit locks: 5407 units (3988 locked-in, 1419 locked-out)
#> │└─action locks: none
#> └─model
#> │├─status: not built yet (will build in solve())
#> │├─objectives: 2 registered (Total cost, Total impact)
#> │├─method: epsilon_constraint (primary: Total impact)
#> │├─solver: cbc
#> │└─checks: ok
#> # ℹ Use `x$data` to inspect stored tables and model snapshots.Printing p again after adding actions, effects,
constraints, and objectives helps confirm that the problem has moved
from a base spatial specification to a fully defined optimisation model
ready to be solved.
res <- solve(p, process = TRUE)
plot_tradeoff(res, label_runs = TRUE)
The resulting SolutionSet can be used to compare
efficient alternatives both numerically and spatially. The trade-off
plot helps assess how the solutions balance the competing objectives,
whereas the spatial action maps show where conservation and restoration
are allocated in each efficient solution. This is particularly valuable
because different Pareto-efficient solutions may achieve similar totals
while distributing actions very differently across the landscape.
If desired, the spatial outputs can also be rendered with a consistent manual palette to improve visual comparison across solutions. For example:
plot_spatial_actions(
res,
fill_values = c(
restoration = "#f38b3d",
conservation = "#97df83"
),
runs = c(1, 2, 3, 4),
base_alpha = 0.0
)
The resulting SolutionSet can be used to compare
efficient alternatives both numerically and spatially. The trade-off
plot helps assess how the solutions balance the competing objectives,
whereas the spatial action maps show where conservation and restoration
are allocated in each efficient solution. This is particularly valuable
because different Pareto-efficient solutions may achieve similar totals
while distributing actions very differently across the landscape.
This example illustrates one of the main strengths of
multiscape: spatial planning problems can be formulated in
terms of management actions, action-specific
effects, feasibility rules,
constraints, and multiple competing
objectives. Rather than returning only a single optimum, the
package can generate and analyse alternative efficient solutions that
reveal trade-offs relevant for decision-making.
To explore the package further, see the function reference on the
package website and the documentation of key functions such as
create_problem(), add_actions(),
add_effects(),
add_constraint_targets_relative(), and
solve().
For multi-objective workflows, the most relevant functions are
set_method_weighted_sum(),
set_method_epsilon_constraint(), and
set_method_augmecon().
If you find a bug or want to suggest an improvement, please open an issue at:
https://github.com/josesalgr/multiscape/issues
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.