Spatial prioritization¶
Two ways of setting conservation priorities, side by side: ranking every cell of a landscape (below), and selecting the best set of planning units by optimization, with targets, budgets, spatial penalties and constraints, portfolios, multi-action plans and the evaluation of the solutions (Optimization onwards). Both read the same rasters and share their pre-processing, and each can start or check the other.
Ranking the cells of a landscape from the least to the most important for
conservation, given where a set of features (species, habitats, ecosystem
services, carbon...) occur: the core of systematic conservation planning. The
ranking balances all features at once, weighted, with costs and hierarchical
masks (for example, protected areas kept to the end), and its performance curves
tell how much of every feature the best 10 %, 20 %... of the landscape hold.
It lives in the ls.prioritize module.
The method is Zonation 5 (Moilanen et al. 2022): the cells are sorted by their marginal loss, the loss to the features of removing a cell at the point where it is removed, with the core-area (CAZ, driven by the features that lose the largest fraction) or additive-benefit (ABF) rules, and sorted again until the order is consistent. Landschaft gives the rank maps and curves of the z5 program (v2.4) bit for bit, faster, with the curves of the ranking it returns (Zonation's own report those of the order before its last sort). The tutorial walks through a project, and the course chapter Conservation priorities explains the method.
import landschaft as ls
res = ls.prioritize.zonation(["sp1.tif", "sp2.tif", "sp3.tif"], weights=[2, 1, 1],
cost="land_price.tif", hierarchic_mask="protected.tif")
res.rank # 0 = removed first ... ~1 = highest priority
res.feature_curves # fraction of every feature kept, by fraction removed
res.write("zonation_output") # rankmap.tif and Zonation's CSV files
Ranking¶
zonation
¶
zonation(
features,
*,
weights=None,
rule: str = "CAZ2",
rule_param: float | None = None,
abf_z=None,
names=None,
weight_groups=None,
group_weights: dict | None = None,
groups=None,
thresholds=None,
zero_mode: str = "all",
keep_zeros=None,
extract=None,
cost=None,
cost_in_ranking: bool = True,
hierarchic_mask=None,
analysis_area=None,
area: int = 0,
solution=None,
bits: int = 8,
max_iterations: int = 500,
ranking_error: float = 0.01,
smoothing_factor: float = 0.0,
stop_on_plateau: bool = False,
curves_interval: float = 0.0001,
seed: int | None = None,
condition=None,
renormalize=False,
connectivity=None,
self_multiply=False,
interaction=None,
interaction2=None,
matrix_connectivity=None,
matrices: dict | None = None,
retention=None,
preprocessing: bool = False,
transformed=False,
impact_groups=None,
impact_bins=None,
impact=None,
cumulative_impact=None,
nodata=None,
storage=None,
n_threads: int | None = None,
) -> ZonationResult
Rank the cells of a landscape for conservation, as Zonation 5 does.
Every feature (species distribution, habitat, ecosystem service...) is
normalized to sum 1 and quantized to bits bits; the cells are then
ordered from the least to the most important by their marginal loss: the
loss to the features, weighted, of removing a cell at the point where it is
removed, with the other cells removed before it. Zonation iterates stable
sorts of the cells by that loss until the order is consistent (the mean
ranking error below ranking_error %). The result is the same, bit for
bit, as the z5 program (v2.4) on one thread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
Raster files (every band is a feature), 2-D arrays or DataArrays, or a list of them; or a 3-D array/DataArray (features, rows, columns), NumPy or dask. All on the same grid. |
required | |
weights
|
One weight per feature (Zonation's |
None
|
|
rule
|
str
|
Marginal loss rule: |
'CAZ2'
|
rule_param
|
float | None
|
ABF exponent z (0.25) or CAZP/CAZPW exponent p (1). |
None
|
abf_z
|
ABF exponent per feature ( |
None
|
|
names
|
Feature names for the tables. |
None
|
|
weight_groups
|
Weight group of each feature (-1: none) and the total weight of each group
( |
None
|
|
group_weights
|
Weight group of each feature (-1: none) and the total weight of each group
( |
None
|
|
groups
|
Output group of each feature (-1: none), summarized in |
None
|
|
thresholds
|
Per feature, values below become 0. |
None
|
|
zero_mode
|
str
|
|
'all'
|
extract
|
Per feature, a value to extract from a categorical raster (cells with it
keep it, the others become NoData; |
None
|
|
cost
|
Cost of every cell. With |
None
|
|
hierarchic_mask
|
Integer levels 0-255 per cell: lower levels are removed first. |
None
|
|
analysis_area
|
Raster of analysis areas: cells outside (0, or other than |
None
|
|
area
|
Raster of analysis areas: cells outside (0, or other than |
None
|
|
solution
|
The ranking to evaluate with |
None
|
|
bits
|
int
|
Quantization of the feature values: 8 ( |
8
|
max_iterations
|
int
|
The iteration: at most |
500
|
ranking_error
|
int
|
The iteration: at most |
500
|
smoothing_factor
|
int
|
The iteration: at most |
500
|
stop_on_plateau
|
int
|
The iteration: at most |
500
|
curves_interval
|
float
|
Fraction of the cells between two rows of the curves (0.0001: 10 000 rows). |
0.0001
|
seed
|
int | None
|
Seed of |
None
|
condition
|
Per feature, a raster of the condition of its habitat, 0-1 ( |
None
|
|
renormalize
|
Per feature (or for all), rescale a conditioned feature to level 1. |
False
|
|
connectivity
|
Per feature, a |
None
|
|
self_multiply
|
Per feature (or for all): multiply the smoothed feature by the feature. |
False
|
|
interaction
|
Per feature, an |
None
|
|
interaction2
|
Per feature, an |
None
|
|
matrix_connectivity
|
Per feature, |
None
|
|
matrices
|
Per feature, |
None
|
|
retention
|
Per feature (Zonation's |
None
|
|
preprocessing
|
bool
|
Also compute Zonation's pre-processing maps ( |
False
|
transformed
|
Per feature (or |
False
|
|
impact_groups
|
Per feature, its impact group (-1: none), for the impact scenarios. |
None
|
|
impact_bins
|
Per feature, the value classes its impacts are reported in: |
None
|
|
impact
|
An |
None
|
|
cumulative_impact
|
An |
None
|
|
nodata
|
NoData value of inputs without one (NaN is always NoData). |
None
|
|
storage
|
A directory for temporary files: the large arrays (the quantized
features, the rows of the ranking and its work arrays) are then
memory-mapped files that the operating system pages to disk when they
do not fit in memory, for maps larger than the RAM (out-of-core). The
files are deleted when no longer needed. |
None
|
|
n_threads
|
int | None
|
Threads (default: all cores but one). |
None
|
Returns:
| Type | Description |
|---|---|
ZonationResult
|
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> rng = np.random.default_rng(0)
>>> species = rng.random((3, 20, 30)) ** 3 # three features
>>> res = ls.prioritize.zonation(species, weights=[2, 1, 1])
>>> res.rank.shape
(20, 30)
>>> float(res.rank.max()) < 1.0
True
>>> round(float(res.feature_curves.iloc[0, 1]), 4) # nothing removed yet
1.0
ZonationResult
dataclass
¶
ZonationResult(
rank: object,
feature_curves: DataFrame,
summary_curves: DataFrame,
density_curves: DataFrame,
group_curves: DataFrame | None,
features: DataFrame,
iterations: int,
ranking_error: float,
auc: float,
history: DataFrame,
order: ndarray,
info: dict = dict(),
preprocessing: dict = dict(),
transformed: dict = dict(),
hierarchic_mask_ranks: dict = dict(),
impacts: dict = dict(),
_ctx: dict = dict(),
)
The result of zonation().
Attributes:
| Name | Type | Description |
|---|---|---|
rank |
DataArray
|
Priority rank of every cell with data, from 0 (first removed, lowest priority) to (n - 1) / n (last removed, highest priority); NaN elsewhere. |
feature_curves |
DataFrame
|
Fraction remaining of every feature (columns) as the lowest-ranked cells
are removed ( |
summary_curves, density_curves |
DataFrame
|
Mean, weighted mean, maximum and minimum of the curves (by the sign of the weights), remaining area and cost; and the loss rates of the means. |
group_curves |
DataFrame or None
|
Mean, weighted mean and minimum per output group. |
features |
DataFrame
|
Per feature: input number, name, weight, the sum of its values, cells with values above zero, cells without data, quantization error (%). |
iterations |
int
|
Sorting iterations run (0 for |
ranking_error |
float
|
Mean ranking error of the last iteration (%). |
auc |
float
|
Area under the mean curve of the positively weighted features. |
history |
DataFrame
|
Ranking error (%), number of cells out of order and mean rank change (%) per iteration. |
order |
ndarray
|
Indices (row-major) of the cells in removal order. |
preprocessing |
dict
|
With |
transformed |
dict
|
With |
hierarchic_mask_ranks |
dict
|
With a hierarchic mask: the lowest rank of each of its levels. |
impacts |
dict
|
With impact scenarios: |
area_statistics
¶
Statistics of areas of the landscape: their size, mean rank, the share
of every feature they hold and how rich they are relative to their size
(Zonation's area_statistics_external).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
areas
|
Raster of area numbers (0 or NoData: none) on the features' grid. |
required |
Returns:
| Type | Description |
|---|---|
AreaStatistics
|
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> x = np.random.default_rng(0).random((3, 20, 30))
>>> res = ls.prioritize.zonation(x)
>>> zones = np.repeat([[1, 2, 3]], 20 * 10, axis=0).reshape(20, 30)
>>> st = res.area_statistics(zones)
>>> st.areas[["area", "size"]].values.tolist()
[[1, 200], [2, 200], [3, 200]]
area_statistics_rank_range
¶
area_statistics() of the cells ranked in [min_rank, max_rank]
(Zonation's area_statistics_rank_range); the mask is in the result.
Examples:
area_statistics_separate_areas
¶
area_statistics() of the separate (8-connected) areas of cells
ranked in [min_rank, max_rank] of at least min_area_size cells
(Zonation's area_statistics_separate_areas).
Examples:
solution_comparison
¶
Compare the cells ranked in [min_rank, max_rank] with the cells of another
solution with values in [min_value, max_value] (Zonation's
solution_comparison).
Returns:
| Type | Description |
|---|---|
dict
|
|
Examples:
histogram
¶
How many features keep which fraction in the cells ranked in [min_rank,
max_rank]: per weight class and output group, the count of features per bin
of coverage (Zonation's histogram).
Examples:
flexibility_map
¶
How far each cell could move in the ranking before its marginal loss, at the
other position, differs from that of the cell there by max_error %
(Zonation's flexibility_map): up, down and their sum both,
as fractions of the cells.
Examples:
postprocess
¶
Run Zonation's post-processing calls and write its files into directory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
calls
|
A post-processing file (its paths are relative to it), its text, or a list
of |
required | |
directory
|
Output directory. |
required |
Returns:
| Type | Description |
|---|---|
list of pathlib.Path
|
The files written. |
Examples:
save
¶
write
¶
Write the rank map and the tables as Zonation 5 does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Output directory (created if needed): |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The directory. |
Examples:
>>> import tempfile
>>> import numpy as np
>>> import landschaft as ls
>>> res = ls.prioritize.zonation(np.random.default_rng(0).random((2, 10, 12)))
>>> out = res.write(tempfile.mkdtemp())
>>> sorted(p.name for p in out.iterdir())
['feature_curves.csv', 'feature_density_curves.csv', 'features_info.csv', 'rankmap.tif', 'summary_curves.csv']
Large maps¶
The features are read in blocks of rows (files, NumPy, xarray or dask arrays; dask stacks are computed block by block, never whole) and quantized to one or two bytes per value, kept as runs of NoData, zeros and values: a feature costs about its occurrences, however many cells it is zero in, so thousands of species ranges fit where Zonation stores every feature in every cell. During the ranking every cell's occurrences are a compact row (a bitmap or layer gaps, whichever is smaller) and the work arrays take about 130 bytes per cell with data.
For maps larger than the memory, storage puts the large arrays in memory-mapped
temporary files in a directory (on a fast disk), which the operating system pages
as needed; the reads and writes of the ranking are in sequence or nearly so. The
result is the same bit for bit; the files are deleted when no longer needed.
import dask.array as da
stack = da.stack([da.from_zarr(f"ranges.zarr/{s}") for s in species]) # (features, y, x)
res = ls.prioritize.zonation(stack, storage="D:/scratch", n_threads=16)
res.write("global_ranking")
The ranking is limited to 2^32 - 1 cells with data and 65 535 features.
Zonation projects¶
Existing Zonation 5 projects run as they are: the settings file, its feature list and link files (weight groups, conditions, retention), with the switches of the z5 command line.
run_project
¶
Run a Zonation 5 project (see read_project()).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
As |
required | |
flags
|
As |
required | |
mode
|
As |
required | |
output
|
Directory to write Zonation's output files into (optional). |
None
|
|
**overrides
|
Other |
{}
|
Returns:
| Type | Description |
|---|---|
ZonationResult
|
|
Examples:
>>> import tempfile
>>> from pathlib import Path
>>> import numpy as np, rasterio
>>> import landschaft as ls
>>> d = Path(tempfile.mkdtemp())
>>> for i in range(2):
... with rasterio.open(d / f"sp{i}.tif", "w", driver="GTiff", height=5, width=6, count=1,
... dtype="float32", crs="EPSG:32633",
... transform=rasterio.transform.from_origin(0, 500, 100, 100)) as f:
... f.write(np.random.default_rng(i).random((5, 6), dtype=np.float32), 1)
>>> _ = (d / "features.txt").write_text("filename\nsp0.tif\nsp1.tif\n")
>>> _ = (d / "settings.z5").write_text("feature list file = features.txt\n")
>>> res = ls.prioritize.run_project(d / "settings.z5", mode="CAZ2")
>>> res.rank.shape
(5, 6)
read_project
¶
The zonation() arguments of a Zonation 5 project.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
The project's settings file ( |
required | |
flags
|
The z5 command-line switches that enable its parts, as a string or list:
|
''
|
|
mode
|
str | None
|
The marginal loss rule (z5's |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Keyword arguments of |
Examples:
>>> import tempfile
>>> from pathlib import Path
>>> import numpy as np, rasterio
>>> import landschaft as ls
>>> d = Path(tempfile.mkdtemp())
>>> for i in range(2):
... with rasterio.open(d / f"sp{i}.tif", "w", driver="GTiff", height=5, width=6, count=1,
... dtype="float32", crs="EPSG:32633",
... transform=rasterio.transform.from_origin(0, 500, 100, 100)) as f:
... f.write(np.random.default_rng(i).random((5, 6), dtype=np.float32), 1)
>>> _ = (d / "features.txt").write_text("weight filename\n2.0 sp0.tif\n1.0 sp1.tif\n")
>>> _ = (d / "settings.z5").write_text("feature list file = features.txt\n")
>>> kw = ls.prioritize.read_project(d / "settings.z5", "-w", mode="ABF")
>>> kw["weights"], kw["rule"]
([2.0, 1.0], 'ABF')
Connectivity¶
Dispersal kernels for the connectivity transforms of the features (the
connectivity, interaction and matrix_connectivity arguments of
zonation): a feature smoothed by the dispersal of its species, weighted by the
connectivity of a resource, or mixed with others by a matrix.
Kernel
dataclass
¶
A dispersal kernel of Zonation 5's connectivity transforms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shape
|
str
|
|
required |
distance
|
float
|
In cells. |
required |
cut
|
float
|
|
1.0
|
Examples:
Interaction
dataclass
¶
Interaction(
kernel: Kernel,
resource: object,
positive: bool = True,
beta: float = 1.0,
normalize: bool = True,
)
An interaction connectivity transform (Zonation's -i/-I): a feature
weighted by the connectivity of a resource (positive) or reduced by it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kernel
|
Kernel
|
Dispersal kernel from the resource. |
required |
resource
|
object
|
Raster of the resource (path, DataArray or array). |
required |
positive
|
bool
|
|
True
|
beta
|
float
|
Strength of the negative interaction. |
1.0
|
normalize
|
bool
|
Negative interactions: keep the feature's level ( |
True
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> rng = np.random.default_rng(0)
>>> species = rng.random((2, 30, 40))
>>> food = rng.random((30, 40)) # a resource of species 0
>>> ia = ls.prioritize.Interaction(ls.prioritize.Kernel("negexp", 3, 3), food)
>>> res = ls.prioritize.zonation(species, interaction=[ia, None])
>>> res.iterations > 0
True
convolve
¶
convolve(image: ndarray, kernel: Kernel, workers: int = 1) -> ndarray
Convolve a raster with a kernel normalized to sum 1, as Zonation 5 does.
FFT convolution in blocks of the kernel's size, the blocks' results added as float32 in Zonation's order: the smoothing of its connectivity transforms.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
ndarray
|
2-D array (rows, columns); cells outside the data should be 0. |
required |
kernel
|
Kernel
|
A |
required |
workers
|
int
|
FFT threads. |
1
|
Returns:
| Type | Description |
|---|---|
ndarray
|
float32, the image's shape. |
Examples:
Post-processing¶
Analyses of a ranking (methods of ZonationResult): statistics of areas
(external zones, the top fraction, separate high-priority areas), comparison with
another solution, histograms of the features' coverage and the flexibility map;
ZonationResult.postprocess runs Zonation's post-processing files.
AreaStatistics
dataclass
¶
Bases: Saveable
Statistics of areas of a ranking (area_statistics()).
Attributes:
| Name | Type | Description |
|---|---|---|
areas |
DataFrame
|
Per area (and weight class or output group, column |
coverage |
dict
|
Per weight class / group, a DataFrame: the fraction of every feature in every area. |
mask |
ndarray or None
|
The areas (0: outside), when they come from the ranking. |
Impact assessment¶
Scenarios of land-use change and pressures, evaluated for the features and
applied to them before the ranking (the impact, cumulative_impact,
impact_groups and impact_bins arguments of zonation): the loss around
impact sources, per footprint and buffer zone, and the direct habitat loss and
cumulative pressures of every impact group.
ImpactScenario
dataclass
¶
ImpactScenario(
sources: object,
impacts: list,
name: str = "scenario",
subareas: object = None,
area_unit: str = "",
impact_unit: str = "",
area_factor: float = 1.0,
rasters: bool = True,
)
An impact assessment scenario (Zonation's -s): land-use change at sources.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sources
|
object
|
Raster of the impact sources (integer values; 0: none). |
required |
impacts
|
list
|
The |
required |
name
|
str
|
Project id (the output directory's name). |
'scenario'
|
subareas
|
object
|
Raster of subareas (integers, 0: none) reported apart. |
None
|
area_unit
|
str
|
Names of the units of area and of the features' values, and the area of a cell in the area unit. |
''
|
impact_unit
|
str
|
Names of the units of area and of the features' values, and the area of a cell in the area unit. |
''
|
area_factor
|
str
|
Names of the units of area and of the features' values, and the area of a cell in the area unit. |
''
|
rasters
|
bool
|
Write the impact rasters of the groups. |
True
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> P = ls.prioritize
>>> species = np.random.default_rng(0).random((2, 30, 40))
>>> road = np.zeros((30, 40), np.uint16)
>>> road[15, :] = 1 # source 1: a planned road
>>> sc = P.ImpactScenario(road, [P.ImpactSource(group=1, source=1, footprint=100,
... buffers=((3, 50),))]) # 50 % loss up to 3 cells
>>> res = P.zonation(species, impact_groups=[1, -1], impact=sc)
>>> zones = res.impacts["zones"]
>>> sorted(zones["zone"].unique().tolist()) # footprint and buffer ring
[0, 1]
ImpactSource
dataclass
¶
An impact source of an impact group (a line of Zonation's impact buffers file).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group
|
int
|
Impact group (the features with this |
required |
source
|
int
|
Value of the source's cells in the impact source raster. |
required |
footprint
|
float
|
Loss (%) in the source's own cells. |
required |
buffers
|
tuple
|
|
()
|
CumulativeScenario
dataclass
¶
CumulativeScenario(
name: str = "cumulative",
subareas: object = None,
area_unit: str = "",
impact_unit: str = "",
area_factor: float = 1.0,
rasters: bool = True,
direct: str | None = None,
direct_rasters: list = list(),
direct_matrix: object = None,
cumulative: str | None = None,
pressures: list = list(),
cumulative_matrix: object = None,
aggregation: str = "add",
functions: dict = dict(),
)
A cumulative impact assessment scenario (Zonation's -e).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
As |
'cumulative'
|
subareas
|
str
|
As |
'cumulative'
|
area_unit
|
str
|
As |
'cumulative'
|
impact_unit
|
str
|
As |
'cumulative'
|
area_factor
|
str
|
As |
'cumulative'
|
rasters
|
str
|
As |
'cumulative'
|
direct
|
str | None
|
Direct habitat loss: |
None
|
direct_rasters
|
list
|
Loss rasters (fractions) and the weights (impact groups x rasters). |
list()
|
direct_matrix
|
list
|
Loss rasters (fractions) and the weights (impact groups x rasters). |
list()
|
cumulative
|
str | None
|
Cumulative pressures: |
None
|
pressures
|
list
|
The |
list()
|
cumulative_matrix
|
object
|
variant1: weights (impact groups x pressures). |
None
|
aggregation
|
str
|
variant2: |
'add'
|
functions
|
dict
|
Response functions, |
dict()
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> P = ls.prioritize
>>> species = np.random.default_rng(0).random((2, 30, 40))
>>> pressure = np.linspace(0, 1, 30 * 40, dtype=np.float32).reshape(30, 40)
>>> sc = P.CumulativeScenario(cumulative="variant1", pressures=[P.Pressure(pressure)],
... cumulative_matrix=np.array([[1.0]]),
... functions={(1, None): [(0, 0), (1, 0.5)]}) # up to 50 % loss
>>> res = P.zonation(species, impact_groups=[1, 1], cumulative_impact=sc)
>>> sorted(res.impacts["cumulative"]["phase"].unique().tolist())
['cumulative']
Pressure
dataclass
¶
A pressure raster of a cumulative impact scenario.
Optimization¶
A problem of planning units (raster cells, polygons, H3 cells or table rows), features, an
objective, targets, penalties and constraints, compiled to a mixed-integer linear model and solved
to a guaranteed gap. The grammar and the models are prioritizr's (Hanson et al.): compile() gives
prioritizr 8.0.6's model bit for bit and the solvers reach the same optima as prioritizr on Gurobi.
HiGHS is built in, with Landschaft's own engines in front of it for the problems it finds hard
(covering problems of millions of units, budgeted objectives, boundary penalties by minimum cuts,
multi-action plans); Gurobi is used when installed and asked for. Coming from R or prioripy? The
migration guide lists the equivalent of every function.
import landschaft as ls
P = ls.prioritize
cost = ls.load_raster(P.example_data("cost"))
features = ls.load_raster(P.example_data("features"))
protected = ls.load_raster(P.example_data("protected"))
p = (P.problem(cost, features)
.add_min_set_objective()
.add_relative_targets(0.2)
.add_locked_in_constraints(protected)
.add_boundary_penalties(0.0005)
.add_binary_decisions()
.add_highs_solver(gap=0.05))
sol = p.solve()
sol.status, round(sol.gap, 3)
problem
¶
problem(
x, features=None, *, cost_column=None, rij=None, contribution=None, zones=None
) -> Problem
A conservation planning problem (prioritizr's problem()).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
The planning units and their cost: a raster (the cells with a cost are
the planning units), polygons (a GeoDataFrame) or a table, both with a
|
required | |
features
|
With a raster: the features' rasters on the same grid (a |
None
|
|
cost_column
|
The cost column of a table. |
None
|
|
rij
|
Marxan-style amounts ( |
None
|
|
contribution
|
Management zones: every planning unit goes to at most one zone, each
with its own cost ( |
None
|
|
zones
|
With |
None
|
Examples:
>>> import landschaft as ls
>>> cost = ls.load_raster(ls.prioritize.example_data("cost"))
>>> features = ls.load_raster(ls.prioritize.example_data("features"))
>>> p = (ls.prioritize.problem(cost, features)
... .add_min_set_objective()
... .add_relative_targets(0.3)
... .add_binary_decisions())
>>> sol = p.solve()
>>> sol.status
'optimal'
>>> bool(sol.coverage["met"].all())
True
Problem
dataclass
¶
Problem(
units: Units,
objective: tuple | None = None,
targets: tuple | None = None,
locked_in: object = None,
locked_out: object = None,
bounds: tuple = (),
decisions: tuple = ("binary",),
solver: tuple = ("highs", {}),
weights: object = None,
subregional: tuple | None = None,
spatial: tuple = (),
portfolio: tuple | None = None,
zone_targets: tuple = (),
mandatory: bool = False,
)
A conservation planning problem (prioritizr's problem()): see problem().
Every add_*_objective that takes a budget takes one number (the most the selected units may
cost); with zones, also one budget per zone (a list in the zones' order, or a mapping zone ->
budget).
add_min_set_objective
¶
add_min_set_objective() -> Problem
Minimum set: meet every target at the least cost.
add_min_shortfall_objective
¶
add_min_shortfall_objective(budget: float) -> Problem
Minimum shortfall: within the budget, the least sum of the targets' shortfalls, each as a
share of its target (weighted by add_feature_weights()).
add_min_largest_shortfall_objective
¶
add_min_largest_shortfall_objective(budget: float) -> Problem
Minimum largest shortfall: within the budget, the least of the largest shortfall of any target, as a share of the target.
add_max_utility_objective
¶
add_max_utility_objective(budget: float) -> Problem
Maximum utility: within the budget, the most of the features' amounts (weighted); no targets.
add_max_features_objective
¶
add_max_features_objective(budget: float) -> Problem
Maximum features: within the budget, the most targets met (weighted).
add_max_wtd_sum_objective
¶
add_max_wtd_sum_objective(budget: float) -> Problem
add_max_utility_objective() under prioritizr 9's name (the model is 8.0.6's, with the
small cost term that breaks ties, which prioritizr 9 dropped).
add_max_n_targets_met_objective
¶
add_max_n_targets_met_objective(budget: float) -> Problem
add_max_features_objective() under prioritizr 9's name (the model is 8.0.6's, with the
small cost term that breaks ties, which prioritizr 9 dropped).
add_max_cover_objective
¶
add_max_cover_objective(budget: float) -> Problem
Maximum cover: within the budget, the most features held at all (any amount above zero, weighted); no targets.
add_max_phylo_div_objective
¶
add_max_phylo_div_objective(budget: float, tree) -> Problem
Maximum phylogenetic diversity: within the budget, the most total length of the branches
of a phylogeny with a target met below them (Faith 1992). tree: an ls.bio tree
(read_tree()) or a Newick string, whose tips are the features.
add_max_phylo_end_objective
¶
add_max_phylo_end_objective(budget: float, tree) -> Problem
Maximum phylogenetic endemism: as add_max_phylo_div_objective(), every branch's
length divided by the total amount of the features below it (Rosauer et al. 2009).
add_min_extinction_risk_objective
¶
add_min_extinction_risk_objective(budget: float, z=0.25, segments: int = 16) -> Problem
Minimum extinction risk (our extension): within the budget, the least sum over the features
(weighted) of the risk the species-area relationship gives, 1 - (held / total)^z (Pereira &
Daily 2006; z = 0.25 by default, one value or one per feature), as a piecewise-linear function
through segments + 1 points of the curve (exact at them, slightly above in between). No
targets.
add_min_penalties_objective
¶
add_min_penalties_objective(budget: float) -> Problem
Minimum penalties: meet the targets within the budget with the least penalties (the penalties come in a later phase: until then any solution that meets them is optimal).
add_relative_targets
¶
add_relative_targets(targets, zone=None) -> Problem
Targets as shares (0-1) of every feature's total amount (one value or one per feature).
With zones, the targets are met by the sum over the zones of the features' amounts in them,
each counted by its zone's contribution: a share of the feature's total (the base features
of the contributions; with every zone's own features, the sum of its totals in the zones, as
prioritizr). zone: targets of one zone (shares of the feature's total in that zone) or of a
list of zones, added to the others (targets of the same zones replace the earlier ones). A
matrix (features x zones) gives every zone its own targets (prioritizr's form).
add_absolute_targets
¶
add_absolute_targets(targets, zone=None) -> Problem
Targets as amounts of every feature (one value or one per feature); zone as in
add_relative_targets().
add_manual_targets
¶
add_manual_targets(targets: DataFrame) -> Problem
Targets from a table, one row per target: feature (name), type ("relative" or
"absolute"), target and, optionally, sense (">=", the default, "<=" or
"="). A feature may have several targets, or none. With zones, an optional zone
column: a zone or a list of zones whose amounts the target adds up (default: every zone; a
relative target as in add_relative_targets()).
add_subregional_targets
¶
add_subregional_targets(regions, targets: DataFrame) -> Problem
Targets within sub-regions (an extension: prioritizr has none), added to the other
targets: one row per target, region, feature (name), target, optionally type
("relative", the default, or "absolute") and sense (">=", "<=", "=").
A relative target is a share of the feature's total in the region, over every unit with or
without a cost, as the other relative targets are. With zones, an optional zone column (a
zone or a list of zones, default every zone), as in add_relative_targets().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
regions
|
The region of every unit: with raster units, a raster on their grid (NaN: no region); with tables and polygons, a column name or one value per row (missing: no region). |
required | |
targets
|
DataFrame
|
The targets (a DataFrame or a dict of columns). |
required |
add_loglinear_targets
¶
add_loglinear_targets(
lower_bound_amount: float,
lower_bound_target: float,
upper_bound_amount: float,
upper_bound_target: float,
cap_amount: float | None = None,
cap_target: float | None = None,
abundances=None,
) -> Problem
Targets that fall log-linearly with a feature's total amount (Rodrigues et al. 2004):
lower_bound_target (a share) up to lower_bound_amount, upper_bound_target from
upper_bound_amount, linear in log10(amount) between them; with a cap, features of at
least cap_amount get the absolute target cap_target. abundances: the amounts
(default: every feature's total).
add_feature_weights
¶
add_feature_weights(weights) -> Problem
Weights of the features (of the targets, when there are targets) in the objectives that add them up: min shortfall, max utility, max features, max cover (ignored, with a warning, by min set and min largest shortfall). With zones and no targets (max utility, max cover): one weight per feature (the same in every zone) or a features x zones matrix.
add_boundary_penalties
¶
add_boundary_penalties(
penalty: float, edge_factor: float = 0.5, data=None, zones=None
) -> Problem
Penalise the boundary of the selection (more compact solutions): penalty per unit of
exposed boundary length. edge_factor: the share of the boundary at the edge of the study
area (not shared with another planning unit) that counts. data: a boundary matrix of every
unit (shared lengths, every unit's perimeter on the diagonal); by default from the planning
units' raster grid or polygons.
With zones: edge_factor one or one per zone, and zones how much the boundary between
two zones counts: "same" (the default, prioritizr's identity matrix: the boundary of every
zone's units), "any", or a symmetric zones x zones matrix (values from -1 to 1; the diagonal
for units of the same zone, the other cells for neighbours in different zones; a DataFrame by
zone names works too).
add_connectivity_penalties
¶
add_connectivity_penalties(penalty: float, data, zones=None) -> Problem
Reward selecting connected units: penalty x the connectivity of every pair selected
together (a symmetric matrix of every unit; the diagonal rewards a unit alone). With zones,
zones as in add_boundary_penalties() (how much a connection between two zones
counts).
add_asym_connectivity_penalties
¶
add_asym_connectivity_penalties(penalty: float, data, zones=None) -> Problem
Penalise asymmetric connections that the selection breaks: penalty x the connectivity
from a selected unit to one not selected (a matrix of every unit, rows the sources). With
zones, zones as in add_boundary_penalties() (it need not be symmetric).
add_linear_penalties
¶
add_linear_penalties(penalty: float, data) -> Problem
Penalise selecting units by a value of each (penalty x value): a raster on the units'
grid, a column name, or one value per unit. With zones, penalty one or one per zone (a list
or a mapping) and data the same values in every zone or a mapping zone -> values (zones not
given: 0) or a raster with a layer per zone.
add_linear_constraints
¶
add_linear_constraints(threshold: float, sense: str, data) -> Problem
The sum over the selected units of a value of each (as add_linear_penalties()) must
be >=, <= or = the threshold. With zones, the sum over every zone (data as
in add_linear_penalties()).
add_contiguity_constraints
¶
add_contiguity_constraints(data=None, zones=None) -> Problem
The selected units must form one contiguous patch. data: an adjacency matrix of every
unit; by default units that share an edge (raster cells, 4 directions) or that touch (polygons).
The model is exact: a spanning arborescence of the selected units, with a flow that rules out cycles. prioritizr's formulation (a tree whose edges always run from a unit to one of a lower index) is a restriction: it misses patches in which a unit has no selected neighbour of a higher index, so it can return a costlier patch than the best, or call a feasible problem infeasible. On HiGHS the problem is solved in three steps: a first patch from that restriction, a bound from directed cuts, then the exact MIP from the first patch. Contiguity stays the hardest spatial rule: give it a time limit on more than a few hundred units.
With zones, zones is a binary symmetric zones x zones matrix: the zones linked through it
form a cluster whose units (in any of its zones) must form one patch; a zone with 0 on the
diagonal is free. By default ("same") the units of every zone form one patch.
add_neighbors_constraints
¶
add_neighbors_constraints(k: int, clamp: bool = True, data=None, zones=None) -> Problem
Every selected unit must have at least k selected neighbours (clamp: at most as many
as it has). data: an adjacency matrix of every unit, by default as
add_contiguity_constraints(). With zones, k one or one per zone, and zones a
binary zones x zones matrix of the zones whose units count as neighbours of a unit in a zone
(default "same": only its own zone's).
add_neighbor_constraints
¶
add_neighbor_constraints(k: int, clamp: bool = True, data=None, zones=None) -> Problem
add_neighbors_constraints() under the name of prioritizr's newer versions.
add_feature_contiguity_constraints
¶
add_feature_contiguity_constraints(data=None) -> Problem
Every feature's units (where it is counted towards its targets) must form one contiguous
patch (the expanded formulation: a column per feature and unit; the exact model of
add_contiguity_constraints() for every feature). data: an adjacency matrix of every
unit for all features, a list of one per feature, or None (as
add_contiguity_constraints()).
add_corridor_constraints
¶
add_corridor_constraints(
terminals, resistance=None, penalty: float = 1.0, data=None
) -> Problem
Corridors (an extension, after prioripy): the terminal units of every network are selected and joined by selected units (a flow from the first terminal to each of the others).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
terminals
|
The terminals of one network (a raster on the units' grid, a column name or one value per unit; > 0 marks a terminal), or a list of networks. |
required | |
resistance
|
Optional: values per unit (as |
None
|
|
data
|
An adjacency matrix of every unit (default: as |
None
|
add_locked_in_constraints
¶
add_locked_in_constraints(locked) -> Problem
Planning units that must be selected: a raster (1 = locked), a column name, a boolean array over the planning units or their (0-based) indices. With zones, a mapping zone -> any of these: the units locked into each zone (a unit in one zone at most, and only where it has a cost).
add_locked_out_constraints
¶
add_locked_out_constraints(locked) -> Problem
Planning units that must not be selected (as add_locked_in_constraints(); with zones,
a mapping zone -> the units kept out of that zone).
add_mandatory_allocation_constraints
¶
add_mandatory_allocation_constraints() -> Problem
With zones: every planning unit goes to a zone (by default a unit may go to none).
add_manual_bounded_constraints
¶
add_manual_bounded_constraints(
data: DataFrame | None = None, *, lower=None, upper=None, zone=None
) -> Problem
The selected share of given planning units must lie between a lower and an upper bound
(prioritizr's add_manual_bounded_constraints; prioripy's add_continuous_bounds).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame | None
|
prioritizr's table: a |
None
|
lower
|
The same as values instead of a table: a number for every planning unit, a raster on the planning units' grid, a column of their table, or one value per planning unit (or per cell or row). A unit with NaN in both is left alone; a NaN (or a side not given) takes the decisions' own bound (0, or 1 or the semicontinuous limit). |
None
|
|
upper
|
The same as values instead of a table: a number for every planning unit, a raster on the planning units' grid, a column of their table, or one value per planning unit (or per cell or row). A unit with NaN in both is left alone; a NaN (or a side not given) takes the decisions' own bound (0, or 1 or the semicontinuous limit). |
None
|
|
zone
|
With zones: the zone of the values (the table takes a |
None
|
Notes
The bounds overwrite the units' bounds in the order the manual constraints were added, after
add_locked_in_constraints() and add_locked_out_constraints(). They must lie within
the decisions' bounds, and with binary decisions be 0 or 1 (prioritizr accepts a share there and
the solver rounds it): both raise an error when the problem is compiled.
add_manual_locked_constraints
¶
add_manual_locked_constraints(
data: DataFrame | None = None, *, status=None, zone=None
) -> Problem
Fix the selected share of given planning units (prioritizr's add_manual_locked_constraints):
a status of 1 locks a unit in, 0 out, a share in between (proportion decisions) fixes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame | None
|
prioritizr's table: |
None
|
status
|
The same as values: a number, a raster on the planning units' grid, a column of their table or one value per planning unit; NaN leaves a unit alone. |
None
|
|
zone
|
With zones: as in |
None
|
add_proportion_decisions
¶
add_proportion_decisions() -> Problem
Any share (0-1) of every planning unit can be selected (a linear program).
add_semicontinuous_decisions
¶
add_semicontinuous_decisions(upper_limit: float) -> Problem
Any share of every planning unit up to upper_limit can be selected.
add_cuts_portfolio
¶
add_cuts_portfolio(number_solutions: int = 10) -> Problem
The best number_solutions distinct solutions, one solve after another, each forbidding
the earlier ones (binary decisions).
add_top_portfolio
¶
add_top_portfolio(number_solutions: int = 10) -> Problem
As add_cuts_portfolio(), every solve to optimality (gap 0).
add_gap_portfolio
¶
add_gap_portfolio(number_solutions: int = 10, pool_gap: float = 0.1) -> Problem
Up to number_solutions distinct solutions whose objective is within pool_gap (relative)
of the optimum's.
add_shuffle_portfolio
¶
add_shuffle_portfolio(
number_solutions: int = 10, remove_duplicates: bool = True, seed: int | None = None
) -> Problem
number_solutions solves of the model with its planning units (and other columns) in random
orders: different solutions of equal quality when there are ties (remove_duplicates: keep
distinct ones).
add_highs_solver
¶
add_highs_solver(
gap: float = 0.1,
time_limit: float | None = None,
n_threads: int | None = None,
presolve: str = "choose",
verbose: bool = False,
) -> Problem
Solve on HiGHS (built into landschaft), to a relative gap (0.1 = 10%).
add_gurobi_solver
¶
add_gurobi_solver(
gap: float = 0.1,
time_limit: float | None = None,
n_threads: int | None = None,
verbose: bool = False,
) -> Problem
Solve on Gurobi (needs the gurobipy package and a Gurobi licence).
add_anneal_solver
¶
add_anneal_solver(
num_reps: int = 10,
num_iters: int | None = None,
start_temp: float = -1.0,
cool_fac: float = 0.0,
num_temp: int = 10000,
spf=(1.0, 2.0),
itimp_type: int = 0,
time_limit: float | None = None,
seed: int = 1,
n_threads: int | None = None,
) -> Problem
Solve on Marxan's simulated annealing (marxan()'s engine), for
minimum-set problems that are hard for a MILP solver: boundary, connectivity and linear penalties,
targets, locked units and other ">=" rows on the planning units, binary decisions.
The problem is Marxan's exactly (prioritizr's boundary penalty is Marxan's BLM, its edge factor
Marxan's fixed boundaries); every run of annealing and iterative improvement is then repaired to
meet every target and trimmed of the units the targets do not need, and the solution is the run
with the lowest objective. Its bound is the linear relaxation without the boundary terms
(a lower bound on the optimum) and status is "optimal" only when the gap closes,
"heuristic" otherwise ("time_limit" if the annealing was cut short).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_reps
|
int
|
Runs (in parallel), from different random reserves. |
10
|
num_iters
|
int | None
|
Annealing iterations per run (default: 1000 per planning unit, at least a million); with a
|
None
|
start_temp
|
float
|
Marxan's schedule: |
-1.0
|
cool_fac
|
float
|
Marxan's schedule: |
-1.0
|
num_temp
|
float
|
Marxan's schedule: |
-1.0
|
spf
|
Every feature's penalty factor during the annealing (Marxan's SPF): a shortfall costs
|
(1.0, 2.0)
|
|
itimp_type
|
int
|
Marxan's iterative improvement after the annealing (0 normal, 3 twice). |
0
|
time_limit
|
float | None
|
Seconds for the runs: every run's annealing gets its share (the runs in waves of
|
None
|
seed
|
int
|
The same seed gives the same solution on any number of threads. |
1
|
n_threads
|
int | None
|
Runs in parallel (default: all cores but one). |
None
|
solve
¶
Solve the problem on its solver (HiGHS unless another was added): a
Solution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
A solution to start from: one value per planning unit, or a
Zonation ranking to build one from: |
None
|
Solution
dataclass
¶
Solution(
selection: object,
values: ndarray,
coverage: DataFrame,
objective: float = nan,
bound: float = nan,
gap: float = nan,
status: str = "",
solver: str = "",
seconds: float = 0.0,
zone_names: tuple = (),
zones: object = None,
_cost: ndarray = None,
)
Bases: Saveable
A solution of a Problem.
Attributes:
| Name | Type | Description |
|---|---|---|
selection |
object
|
The selected share of every planning unit (1 / 0 with binary
decisions): a raster on the planning units' grid (NaN outside the
planning units) or a copy of their table with a |
values |
ndarray
|
The same, one value per planning unit (in their order); with zones, planning units x zones. |
zones |
object
|
With zones: the zone of every planning unit as a raster of zone
numbers (1, 2, ... in the order of |
coverage |
DataFrame
|
Every target's feature, sense, the feature's total amount, the target,
the amount held and the shortfall, absolute and relative, and whether
it is met (prioritizr's |
objective, bound, gap |
The objective of the solution, the solver's bound on the optimum and their relative gap. |
|
status |
str
|
|
solver, seconds |
The solver used and the time it took. |
Portfolio
dataclass
¶
Bases: Saveable
Several solutions of a problem (add_cuts_portfolio() and the others).
Attributes:
| Name | Type | Description |
|---|---|---|
solutions |
list
|
The solutions, best first (cuts, top, gap) or in the order found (shuffle). |
selection |
object
|
Every solution's selection: a raster with one band per solution, or the planning units' table
with columns |
frequency |
object
|
The share of the solutions that select every planning unit (a raster or a table column, as
|
summary |
DataFrame
|
Objective, cost, status and gap of every solution. |
OptimizationProblem
dataclass
¶
OptimizationProblem(
A: csr_matrix,
obj: ndarray,
lb: ndarray,
ub: ndarray,
rhs: ndarray,
sense: tuple[str, ...],
vtype: tuple[str, ...],
modelsense: str,
row_ids: tuple[str, ...],
col_ids: tuple[str, ...],
connected: tuple = (),
corridors: tuple = (),
)
A compiled model: modelsense of obj @ x subject to A x (sense) rhs,
lb <= x <= ub and x of type vtype ("B" binary, "C" continuous,
"I" integer); the layout of prioritizr's compile().
h3_units
¶
H3 hexagons as planning units: the hexagons of an H3 resolution
that cover a cost raster, with their cost.
Pass them to problem() as polygons (cost_column="cost"): the
features' amounts are then summed over every hexagon like any polygon's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cost
|
DataArray
|
The cost raster (with a CRS). Every hexagon that covers a cell with a cost is a planning unit; its cost is the sum of the costs of the cells it covers, each weighted by the exact share of the cell inside it. |
required |
resolution
|
int
|
The H3 resolution (0-15; 7 is about 5 km², 8 about 0.7 km²). |
required |
n_threads
|
int | None
|
Threads for the sums (default: all cores but one). |
None
|
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
One hexagon per row (in the raster's CRS, its six vertices
reprojected): |
Examples:
example_data
¶
Path of an example data set for prioritization: Serra Verde, an imaginary coastal region of 60 x 90 cells of 1 km (SIRGAS 2000 / UTM 23S). Mountains rise in the north-west; the coastal plain has three cities, a highway and two rivers; the ocean in the south-east has no data, which leaves 4825 planning units on land. The data are synthetic, made to behave like real planning data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
('cost', 'features', 'protected', 'urban', 'condition', 'regions', 'carbon', 'terminals', 'threats', 'elevation', 'lines', 'land_cover', 'past_cover', 'footprint', 'ecoregions', 'carbon_potential')
|
For restoration plans ( |
"cost"
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Examples:
Management zones¶
With zones (prioritizr's zones), every planning unit goes to at most one of several zones, each with
its own cost and its own contribution to the features: give problem the costs of the zones (a mapping
zone -> raster, or cost columns) and the contributions, or every zone's own features. Targets are summed
over the zones unless a zone is given. The solution's zones is the zone of every unit; these two
helpers convert between such maps and stacks of zone layers.
category_layer
¶
The zone of every cell of a stack of zone layers (prioritizr's category_layer()): the number
(1, 2, ...) of the layer with the largest value above 0, 0 where none is, NaN where every layer is
NaN. stack: a raster with a layer per zone (a solution's selection) or a mapping zone -> raster.
Examples:
>>> import landschaft as ls
>>> cost = ls.load_raster(ls.prioritize.example_data("cost"))
>>> features = ls.load_raster(ls.prioritize.example_data("features"))
>>> p = (ls.prioritize.problem({"protect": cost, "use": cost * 0.4}, features, contribution={"use": 0.5})
... .add_min_set_objective().add_relative_targets(0.3))
>>> sol = p.solve()
>>> zones = ls.prioritize.category_layer(sol.selection)
>>> zones.attrs["zones"]
{1: 'protect', 2: 'use'}
>>> bool((zones == sol.zones).sum() == (sol.zones >= 0).sum())
True
binary_stack
¶
One 0 / 1 raster per zone from a raster of zone numbers (prioritizr's binary_stack()): a mapping
zone -> raster, ready for add_locked_in_constraints() (for example an existing zoning
plan). zones: the zones' names in the order of their numbers (default: those of a solution's
zones map, else "1", "2", ...); cells with 0 or NaN are in no zone.
Examples:
>>> import landschaft as ls
>>> cost = ls.load_raster(ls.prioritize.example_data("cost"))
>>> features = ls.load_raster(ls.prioritize.example_data("features"))
>>> protected = ls.load_raster(ls.prioritize.example_data("protected"))
>>> p = (ls.prioritize.problem({"protect": cost, "use": cost * 0.4}, features, contribution={"use": 0.5})
... .add_min_set_objective().add_relative_targets(0.3))
>>> existing = protected.fillna(0) # the protected areas: zone 1 (protect)
>>> locked = ls.prioritize.binary_stack(existing, zones=p.zones)
>>> sorted(locked)
['protect', 'use']
>>> sol = p.add_locked_in_constraints(locked).solve()
>>> bool((sol.zones.values[existing.values == 1] == 1).all())
True
Spatial relations¶
The matrices that penalties and constraints read: the length of the boundary units share, which units are adjacent, the connectivity between them from a raster or from distances.
boundary_matrix
¶
The length of the boundary every two units share, and every unit's perimeter on the diagonal
(prioritizr's boundary_matrix()): raster cells with data share xres with the cells above
and below and yres with those left and right, perimeter 2 (xres + yres); polygons share
the length of their common paths, perimeters their own, rounded to 8 decimals.
Examples:
adjacency_matrix
¶
1 for every two adjacent units (prioritizr's adjacency_matrix()): raster cells with data
that share an edge (directions 4) or also a corner (8); polygons that intersect, touching at
a corner included.
Examples:
connectivity_matrix
¶
Connectivity between adjacent units (prioritizr's connectivity_matrix()): the length of
their shared boundary times the mean of y in the two (y: a raster on the units' grid; with
polygons a raster summed over each, or a column name).
Examples:
proximity_matrix
¶
1 for every two units within distance of each other (prioritizr's proximity_matrix()):
raster cell centres, or polygons by their nearest points.
Examples:
decay_connectivity
¶
Connectivity from distances between units as a dispersal probability (Conefor's negative
exponential): exp(-ln(2) d / median), 0.5 at the median dispersal distance, dropped below
min_probability (sparse), 0 on the diagonal and for unreachable pairs (inf, NaN).
distances: units x units (every unit), e.g. least-cost distances from
cost_distance() between unit_points(), commute distances, effective
resistances (circuit) or Euclidean ones.
Examples:
unit_points
¶
A point per unit, (n, 2) coordinates: raster cell centres (every cell), or a point inside every
polygon (shapely's point on surface), to measure distances between units with
cost_distance() or commute_distance().
Examples:
Evaluating solutions¶
The figures of a solution (prioritizr's eval_*, equal bit for bit) and the importance of every
selected unit; the landscape the solution makes (patches, MSPA) and its species-area extinction risk.
They take a Solution, its selection or plain values per planning unit.
eval_n_summary
¶
The number of selected planning units (the sum of the solution's values).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> P.eval_n_summary(p, sol)
summary n
0 overall 534.0
eval_cost_summary
¶
The solution's total cost.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> round(P.eval_cost_summary(p, sol)["cost"].item(), 1) == round(sol.cost, 1)
True
eval_boundary_summary
¶
The solution's boundary length: the edges between selected and other units, plus
edge_factor x the selected units' edges on the outside of the planning units.
data: the boundary matrix (every unit's shared lengths and its perimeter on the diagonal;
default boundary_matrix() of the planning units). With zones: edge_factor one or one per
zone and zones the zones matrix (as add_boundary_penalties()); the rows are the
boundary overall (with the zones matrix) and of every zone's units alone.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> P.eval_boundary_summary(p, sol)
summary boundary
0 overall 382500.0
eval_connectivity_summary
¶
The connectivity among the selected units: the sum of data[i, j] x the solution's values of
units i and j (a symmetric matrix over the units). With zones, zones the zones matrix (default the
identity): the rows are the connectivity overall and within every zone.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> cm = P.connectivity_matrix(cost, features[0])
>>> round(P.eval_connectivity_summary(p, sol, cm)["connectivity"].item(), 2)
1276348.0
eval_asym_connectivity_summary
¶
As eval_connectivity_summary() for an asymmetric matrix (its diagonal counted once).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> import scipy.sparse as sp
>>> cm = P.connectivity_matrix(cost, features[0])
>>> asym = sp.triu(cm).tocsr()
>>> round(P.eval_asym_connectivity_summary(p, sol, asym)["asym_connectivity"].item(), 2)
638174.0
eval_feature_representation_summary
¶
Every feature's total amount and the amount the solution holds, absolute and relative.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> rep = P.eval_feature_representation_summary(p, sol)
>>> rep[["feature", "relative_held"]].round(3)
feature relative_held
0 jaguar 0.200
1 lowland_tapir 0.201
2 harpy_eagle 0.299
3 maned_wolf 0.205
4 golden_lion_tamarin 0.204
5 neotropical_otter 0.201
6 araucaria_forest 0.428
7 mangrove 0.210
eval_target_coverage_summary
¶
eval_target_coverage_summary(
p, solution, include_sense: bool = False, include_zone: bool | None = None
) -> DataFrame
Every target's feature, whether it is met, the feature's total, and the target, the amount held and the shortfall, absolute and relative (shares of the total; 0 when the total is 0).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> cov = P.eval_target_coverage_summary(p, sol)
>>> bool(cov["met"].all())
True
eval_replacement_importance
¶
eval_replacement_importance(
p, solution, rescale: bool = True, n_threads: int | None = None, on=None
)
The replacement cost of every selected unit (Cabeza & Moilanen 2006): how much the objective
worsens when the unit is left out and the problem solved again with the other selected units kept
(infinite when nothing can replace it), rescaled to 0.01-1. One solve per selected unit, in parallel
(n_threads, default all cores but one).
on=ls.helpers() solves them on the helpers of this computer's group too (parallel).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> rc = P.eval_replacement_importance(p, sol)
>>> float(rc.max())
1.0
eval_ferrier_importance
¶
Ferrier et al. (2000)'s irreplaceability of the selected units for every feature and in total (prioritizr's version: the problem needs one ">=" target per feature, amounts >= 0).
Returns a raster with a band per feature and total (or the units' table with those columns): 0
for the units not selected.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> fi = P.eval_ferrier_importance(p, sol)
>>> [str(b) for b in fi.band.values][-2:]
['mangrove', 'total']
>>> float(fi.sel(band="total").max()) > 0
True
eval_rare_richness_importance
¶
The rarity-weighted richness of the selected units (Albuquerque & Beier 2015): the sum over the features of the unit's amount / the feature's largest amount / its total, rescaled to 0.01-1.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> rwr = P.eval_rare_richness_importance(p, sol)
>>> float(rwr.max())
1.0
eval_rank_importance
¶
eval_rank_importance(
p,
solution,
n: int | None = None,
budgets=None,
rescale: bool = True,
by_zone: bool = True,
)
Rank importance (Jung et al. 2021, prioritizr's eval_rank_importance): the problem is solved
with increasing budgets (the min set objective becomes min shortfall), only the solution's units
allowed and each step keeping the units of the step before; a unit's rank is how early it enters
(n = the first budget, ..., 1 = the last), rescaled to 0.01-1. n budgets evenly spaced between
the locked-in units' cost and the solution's, or the budgets themselves.
Returns the per-unit ranks (a raster or the units' table) and a table of every budget's objective,
status and gap. With zones, a layer per zone, and by_zone: a budget per zone (every zone's cost in
equal steps), else one budget over every zone; budgets may then be steps x zones.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> ranks, steps = P.eval_rank_importance(p, sol, n=4)
>>> steps[["budget", "status"]].round(1)
budget status
0 5898.5 optimal
1 11797.0 optimal
2 17695.4 optimal
3 23593.9 optimal
eval_extinction_risk
¶
Every feature's extinction risk by the species-area relationship, 1 - (held / total)^z (z = 0.25 by default, one value or one per feature), and their sum (the expected number of features lost).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> risk = P.eval_extinction_risk(p, sol)
>>> round(float(risk["extinction_risk"].iloc[-1]), 3)
2.423
eval_landscape_metrics
¶
eval_landscape_metrics(
solution, level: str | None = "class", metric=None, zone=None, **kw
) -> DataFrame
Landscape metrics of a raster solution (landscape_metrics() of the map of
the selected cells: class 1 selected, 0 not, outside the planning units no data): e.g. the number,
sizes and shapes of the selected patches. Class-level and patch-level rows of the selected class only.
With zones, the map of the zones (a class per zone, 0 for the units in none; class and patch rows of
the zones, with a zone column), or with zone that zone's units against the others.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> P.eval_landscape_metrics(sol, metric=["np", "ca"])[["metric", "value"]]
metric value
0 ca 53400.0
1 np 13.0
eval_mspa
¶
MSPA (mspa()) of a raster solution: the selected cells as foreground, the other
planning units as background, the cells outside them missing. Returns (the MSPA map, its class
areas). With zones, give the zone whose units are the foreground.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> mspa, areas = P.eval_mspa(sol)
>>> int(areas.loc["Core", "pixels"]) >= 0
True
Areas of a priority surface¶
The separate areas of the top fraction of any priority surface (a Zonation ranking, an importance, a
selection frequency) and what each holds, and the overlap of the top fractions of two surfaces
(Zonation's landscape identification and comparison; on a Zonation ranking, the same areas as
ZonationResult.area_statistics_separate_areas).
eval_landscape_identification
¶
eval_landscape_identification(
surface,
top_fraction: float = 0.1,
*,
min_size: int = 1,
merge_distance: int = 0,
connectivity: int = 8,
problem=None,
features=None,
cost=None,
) -> LandscapeAreas
Landscape identification: the separate areas of the top fraction of a priority surface, and what each holds (Zonation's area statistics of separate areas, over any surface).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
surface
|
The priority surface: a raster (a ranking, an importance, a selection frequency...), a
|
required | |
top_fraction
|
float
|
The share of the surface's cells taken from the top, by Zonation's rule (see Notes). |
0.1
|
min_size
|
int
|
Areas of fewer cells are dropped. |
1
|
merge_distance
|
int
|
Areas this many cells apart (dilations of the top cells) are merged into one; the area keeps
only its own top cells (prioripy's |
0
|
connectivity
|
int
|
8 (Zonation's) or 4. |
8
|
problem
|
A problem on raster planning units of the surface's grid: the features' amounts and the cost in every area, the shares of the features' totals (those relative targets use). |
None
|
|
features
|
Without a problem: feature rasters (bands) on the surface's grid and a cost raster. |
None
|
|
cost
|
Without a problem: feature rasters (bands) on the surface's grid and a cost raster. |
None
|
Returns:
| Type | Description |
|---|---|
LandscapeAreas
|
|
Notes
The cells are ranked by position as Zonation does (position_rank(); ties: the earlier cell in
row order ranks lower) and the top fraction f is the cells ranked at or above 1 - f: on 600
cells, f = 0.1 takes 59 cells (rank 540/600 is just below 0.9 in single precision), as
area_statistics_rank_range() does. Areas are numbered by their first cell in
row order; the numbers of dropped areas are left out (as Zonation's).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> ferrier = P.eval_ferrier_importance(p, p.add_highs_solver(gap=0.01).solve())
>>> lsi = P.eval_landscape_identification(ferrier, 0.1, min_size=5, problem=p)
>>> len(lsi.table) > 0, bool((lsi.table["cells"] >= 5).all())
(True, True)
eval_landscape_comparison
¶
eval_landscape_comparison(
a, b, fractions=(0.05, 0.1, 0.2, 0.3, 0.5)
) -> LandscapeComparison
Landscape comparison: how much the top fractions of two priority surfaces overlap (Zonation's
solution comparison at several fractions, prioripy's eval_landscape_comparison).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Two surfaces on the same grid, of any kind |
required | |
b
|
Two surfaces on the same grid, of any kind |
required | |
fractions
|
The top fractions compared (Zonation's rule: see |
(0.05, 0.1, 0.2, 0.3, 0.5)
|
Returns:
| Type | Description |
|---|---|
LandscapeComparison
|
|
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> features = ls.load_raster(P.example_data("features"))
>>> cost = ls.load_raster(P.example_data("cost"))
>>> caz = P.zonation(features, rule="CAZ1")
>>> abf = P.zonation(features, rule="ABF")
>>> cmp = P.eval_landscape_comparison(caz, abf, fractions=[0.1, 0.3])
>>> cmp.table[["fraction", "both"]].shape
(2, 2)
position_rank
¶
Zonation's ranks of a surface by position: the i-th lowest of the N finite cells gets float32(i * (1 / N)); ties go to the earlier cell in row order first; NaN stays NaN.
On a Zonation rank map this returns the rank map itself, bit for bit.
Examples:
LandscapeAreas
dataclass
¶
Bases: Saveable
The areas of the top fraction of a priority surface (eval_landscape_identification()).
Attributes:
| Name | Type | Description |
|---|---|---|
areas |
object
|
The area of every cell (0: not in an area, NaN outside the surface): a raster like the surface (an array for an array). |
table |
DataFrame
|
Per area: |
top |
object
|
The cells of the top fraction (1, 0, NaN outside), before areas under |
LandscapeComparison
dataclass
¶
Bases: Saveable
Two priority surfaces compared at several top fractions (eval_landscape_comparison()).
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
Per fraction: the cells in the top of |
maps |
object
|
Per fraction (band), Zonation's solution comparison codes: 1 in both tops, 2 only in |
Irreplaceability (ProxIRR)¶
ProxIRR (Baisero et al. 2022): for every target, the share of the feature's spare amount a unit holds (alpha), and per unit the chance it is needed for at least one feature (beta) and the number of features it is essential for plus beta (gamma). The bits are those of the CRAN package proxirr.
eval_proxirr
¶
eval_proxirr(p, triage: bool = False, n_threads: int | None = None) -> ProxIRR
ProxIRR (Baisero et al. 2022) of every planning unit for the problem's targets, as the CRAN package proxirr computes it.
For every >= target of the problem, local is the amount in the unit, global the total
its relative targets are shares of (the whole raster or table, the region for sub-regional
targets) and target its amount; alpha, beta and gamma follow (see the module's notes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p
|
A |
required | |
triage
|
bool
|
proxirr's triage: a feature whose target exceeds what is left counts for nothing (alpha 0) instead of making every unit holding it essential. |
False
|
n_threads
|
int | None
|
Threads (default: all cores but one). |
None
|
Returns:
| Type | Description |
|---|---|
ProxIRR
|
|
Notes
A feature absent from the landscape (global amount 0) gives alpha 0 (proxirr stops with an error).
Targets of sense <= or = are left out.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_relative_targets(0.3)
>>> irr = P.eval_proxirr(p)
>>> float(irr.beta.max()) <= 1.0, float(irr.gamma.max()) >= float(irr.beta.max())
(True, True)
eval_proxirr_marxan
¶
eval_proxirr_marxan(
pu: DataFrame,
spec: DataFrame,
puvspr: DataFrame,
triage: bool = False,
n_threads: int | None = None,
) -> ProxIRR
ProxIRR of a Marxan problem's planning units (proxirr's marxan_alphas, marxan_betas and
marxan_gammas), from the tables marxan() takes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pu
|
DataFrame
|
Marxan's |
required |
spec
|
DataFrame
|
Marxan's |
required |
puvspr
|
DataFrame
|
Marxan's |
required |
triage
|
bool
|
As |
False
|
n_threads
|
bool
|
As |
False
|
Returns:
| Type | Description |
|---|---|
ProxIRR
|
|
Notes
proxirr 0.5 looks a feature's global amount and target up by its position in spec rather than
by its id, so with ids other than 1, 2, ... in order every feature gets another's; and a feature
of puvspr missing from spec stops marxan_betas with an error. Here features are looked
up by id, and those without a target (NaN alphas) are left out of beta and gamma.
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> pu = pd.DataFrame({"id": [1, 2, 3], "cost": [1.0, 1.0, 1.0]})
>>> spec = pd.DataFrame({"id": [10, 20], "target": [4.0, 1.0]})
>>> puvspr = pd.DataFrame({"species": [10, 10, 20, 20], "pu": [1, 2, 2, 3], "amount": [3.0, 2.0, 1.0, 1.0]})
>>> irr = ls.prioritize.eval_proxirr_marxan(pu, spec, puvspr)
>>> irr.alpha["alpha"].tolist()
[1.0, 1.0, 1.0, 1.0]
>>> irr.gamma["gamma"].tolist()
[1.0, 2.0, 1.0]
ProxIRR
dataclass
¶
Bases: Saveable
ProxIRR of every planning unit (eval_proxirr(), eval_proxirr_marxan()).
Attributes:
| Name | Type | Description |
|---|---|---|
beta, gamma |
Per planning unit, like a solution's selection: a raster (NaN outside the units) or the units'
table with a |
|
alpha |
object
|
Per (target, unit): a sparse matrix, targets x planning units (from Marxan tables: the
|
targets |
DataFrame
|
Every target's feature, global amount and amount (from Marxan tables: |
Calibration and trade-offs¶
Candidate solutions over a trade-off (a penalty's weight, cost thresholds, budgets, any parameter), the choice of one by TOPSIS or the knee of the curve, and solving again as a problem changes.
calibrate_penalty
¶
calibrate_penalty(
p,
penalty: str = "boundary",
values=None,
n: int = 9,
data=None,
edge_factor: float = 0.5,
n_threads: int | None = None,
on=None,
) -> TradeOffs
The blended approach: the problem solved with a range of penalty weights (values; default
n values evenly spaced in log scale over 1/100 to 100 times the Cohon et al. penalty), every
candidate's cost and criterion (boundary length, connectivity, ...), and the candidates TOPSIS and the
knee of the curve choose. p: the problem without the penalty. Solves in parallel. on=ls.helpers() solves them on the helpers of this computer's group too (parallel).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> c, f = P.coarsen(cost, features, 6)
>>> p = P.problem(c, f).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> cal = P.calibrate_penalty(p.add_highs_solver(gap=0.05), "boundary", values=[0.001, 0.01, 0.1])
>>> cal.table[["value", "status"]]
value status
0 0.001 optimal
1 0.010 optimal
2 0.100 optimal
>>> best = cal.best("topsis")
calibrate_budget
¶
calibrate_budget(
p,
penalty: str = "boundary",
thresholds=None,
inflation=None,
data=None,
edge_factor: float = 0.5,
n_threads: int | None = None,
on=None,
) -> TradeOffs
The hierarchical approach: the least cost solution first (p, a min set problem without the
penalty, solved to optimality), then the least penalty within cost thresholds above its cost
(thresholds, or the cost x (1 + inflation); default inflation 1e-5 to 4 in 9 steps, the
thresholds rounded up as in prioritizr's tutorial). Solves in parallel.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> c, f = P.coarsen(cost, features, 6)
>>> p = P.problem(c, f).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> cal = P.calibrate_budget(p.add_highs_solver(gap=0.05), "boundary", inflation=[0.1, 0.5, 1.0])
>>> len(cal.table)
3
>>> bool((cal.table["cost"] <= cal.table["value"] + 1e-6).all())
True
cohon_penalty
¶
cohon_penalty(
p, penalty: str = "boundary", data=None, edge_factor: float = 0.5, gap: float = 0.0
)
The penalty of Cohon et al. (1979): the trade-off rate between the solution with the least cost
(p as given, a min set problem without the penalty) and the one with the least penalty
(min penalties within a budget above every unit's cost): |cost difference / criterion difference|,
rounded to 5 decimals. Returns (penalty, a table of the two ideal solutions' cost and criterion).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> c, f = P.coarsen(cost, features, 6)
>>> p = P.problem(c, f).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> value, ideals = P.cohon_penalty(p.add_highs_solver(gap=0.05))
>>> value > 0
True
>>> list(ideals.columns)
['solution', 'cost', 'boundary']
pareto_front
¶
pareto_front(
p, budgets=None, n: int = 10, n_threads: int | None = None, on=None
) -> TradeOffs
The trade-off between cost and representation: the least total shortfall of the targets (min
shortfall objective: the sum of the shortfalls as shares of their targets) within n budgets evenly
spaced up to the least-cost solution's cost (above the cost of the locked-in units, which every budget
includes), or the budgets given. p: a problem with targets (its objective is replaced).
The table adds met (targets met) and efficient (no other candidate is cheaper and not
worse, or better and not dearer).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> front = P.pareto_front(p, n=4)
>>> front.table[["met", "efficient"]]
met efficient
0 1 True
1 3 True
2 2 True
3 8 True
calibrate
¶
calibrate(
make,
values,
criterion,
name: str = "criterion",
n_threads: int | None = None,
on=None,
) -> TradeOffs
Any trade-off parameter: make(value) gives the problem for every value (a feature's weight,
a target, a penalty of any kind...), criterion(problem, solution) the quantity traded off
against the cost (smaller is better). The candidates' table as calibrate_penalty()'s (TOPSIS,
knee), solved in parallel. on=ls.helpers() solves them on the helpers of this computer's group too (parallel).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> def make(w):
... return P.problem(cost, features).add_max_utility_objective(5000).add_feature_weights([w, 1, 1, 1, 1, 1, 1, 1])
>>> def lost(q, s):
... rep = P.eval_feature_representation_summary(q, s)
... return 1 - rep["relative_held"].iloc[1]
>>> cal = P.calibrate(make, [1, 10], lost, name="tapir_lost")
>>> list(cal.table.columns[:3])
['value', 'cost', 'tapir_lost']
TradeOffs
dataclass
¶
Bases: Saveable
Candidate solutions over a trade-off parameter.
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
One row per candidate: |
solutions |
list
|
The candidates' |
best
¶
The candidate the method chooses ("topsis" with equal weights, or "knee").
topsis
¶
TOPSIS (as R's topsis package): the alternatives (rows of decision) scored by their
closeness to the ideal (impacts: "+" larger is better, "-" smaller is better, per column; equal
weights by default). Returns score (0-1, larger is better) and rank (1 = best).
Examples:
knee_point
¶
mean_sd
¶
plangea's choice of weights (post_choose_weights): every criterion (a column of decision)
rescaled to 0-1 over the alternatives (a constant one to 1), the "-" ones (smaller is better)
inverted, and every alternative scored by the mean of its criteria divided by their standard
deviation (good on all of them at once). Returns mean, sd, score (larger is better) and
rank (1 = best). plangea took the mean after adding the deviation as a column (the mean of the
criteria and the deviation); here it is the criteria's.
Examples:
iterative_solve
¶
Solve, change the problem, solve again: update(step, solution, problem) gives the next
problem (for instance from the landscape a ls.simulate step leaves, with the solution's cells
protected by protected_cells() in the simulation's rules), or None to stop. With
lock_previous, every solution's units stay selected in the next problem (protected areas that
accumulate). Returns the solutions and a table (step, cost, units, status).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> def update(step, sol, q):
... return q.add_relative_targets(0.2 + 0.05 * (step + 1))
>>> sols, table = P.iterative_solve(p, update, 3, lock_previous=True)
>>> table["step"].tolist()
[0, 1, 2]
>>> bool(table["cost"].is_monotonic_increasing)
True
protected_cells
¶
The selected cells of a raster solution as a boolean raster (for ls.simulate's restrictions).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> mask = P.protected_cells(sol)
>>> mask.dtype.name
'bool'
Ranking and optimization together¶
Zonation's feature transforms as the features of a problem, the two paradigms compared at the same cost, features from species distribution models, a report of a solution, and problems solved on coarser blocks.
zonation_features
¶
zonation_features(
features,
*,
names=None,
condition=None,
renormalize=False,
connectivity=None,
self_multiply=False,
interaction=None,
interaction2=None,
matrix_connectivity=None,
matrices: dict | None = None,
retention=None,
impact_groups=None,
impact=None,
cumulative_impact=None,
bits: int = 16,
nodata=None,
n_threads: int | None = None,
)
The features as Zonation 5 transforms them before ranking (condition, connectivity kernels,
interactions, matrix connectivity, retention, impacts: see zonation() for each), back in the
features' own units, for an optimization problem (problem(cost, zonation_features(...))).
The values carry Zonation's quantization (bits 16: a relative error of about 1e-5 of the
feature's largest value). Returns a stack (band = the features' names), NaN outside the data.
Examples:
compare_zonation
¶
compare_zonation(
p,
solution=None,
zonation=None,
rule: str = "CAZ2",
n_threads: int | None = None,
**zonation_kw,
) -> ZonationComparison
Compare an optimization solution of p (solved if not given) with a Zonation ranking of the
same features and costs (run with rule and zonation_kw if not given: a
ZonationResult or a rank raster) cut at the solution's cost. Raster planning units.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> c = P.compare_zonation(p, sol)
>>> c.summary["method"].tolist()
['milp', 'zonation']
>>> bool(c.table["milp_met"].all())
True
ZonationComparison
dataclass
¶
ZonationComparison(
table: DataFrame,
summary: DataFrame,
selection: DataArray,
solution: object,
zonation: object,
)
Bases: Saveable
A Zonation ranking and an optimization solution with the same budget.
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
Every feature's share held by the solution ( |
summary |
DataFrame
|
Cost, units and boundary of each. |
selection |
DataArray
|
The two selections (band |
sdm_features
¶
Features from species distribution models (ls.sdm): every result's prediction on env
(its ensemble when it has several models, else its model), as a stack for problem().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
|
required | |
env
|
The predictors (as for |
required | |
threshold
|
|
None
|
Examples:
>>> import landschaft as ls
>>> from sklearn.linear_model import LogisticRegression
>>> occ = ls.load_table(ls.sdm.example_data("bradypus"))
>>> env = ls.load_raster(ls.sdm.example_data("bioclim"))
>>> data = ls.sdm.prepare(occ, env, clean=False, background=500, seed=1)
>>> res = ls.sdm.fit(data, {"glm": LogisticRegression(max_iter=1000)})
>>> f = ls.prioritize.sdm_features({"bradypus": res}, env, threshold="max_tss")
>>> [str(b) for b in f.band.values]
['bradypus']
report
¶
report(
p,
solution=None,
*,
importance: bool = True,
zonation: bool = False,
extinction_z=None,
title: str = "Prioritization",
n_threads: int | None = None,
) -> PrioritizationReport
Solve p (unless a solution is given) and evaluate the solution: the number of units, cost,
boundary (raster and polygon units), the features' representation and the targets' coverage; with
importance the Ferrier irreplaceability of the selected units (problems with one target per
feature) or else their rarity-weighted richness; with extinction_z the species-area extinction
risk; with zonation the comparison with a Zonation ranking at the same cost (raster units).
With zones, every summary has a row overall and one per zone (no irreplaceability, rarity or Zonation
comparison: they need a single zone).
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> sol = p.solve()
>>> r = P.report(p, sol)
>>> sorted(r.tables)
['representation', 'summary', 'targets']
>>> page = ls.plot(r, show=False).save("report.html")
PrioritizationReport
dataclass
¶
PrioritizationReport(
solution: object,
tables: dict,
maps: dict,
comparison: ZonationComparison | None = None,
title: str = "Prioritization",
)
Bases: Saveable
A solution and its evaluation in one object: ls.plot(report) draws it, ls.plot(report).save(
"report.html") writes it as a page that works offline, ls.save(report, folder) its tables and maps.
coarsen
¶
A problem's rasters on blocks of factor x factor cells (for continental problems: solve
the coarse problem, then refine() the selection): every block's cost and amounts are the sums
of its cells' with a cost (the fine planning units; a block without any has none), so the refined
selection holds what the coarse one does. Returns (cost, features).
Examples:
refine
¶
A selection on coarse blocks (a Solution of a coarsen()-ed problem, or its selection
raster) back on the fine grid of like (the fine cost raster): every cell takes its block's value,
NaN where like has none.
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> c, f = P.coarsen(cost, features, 4)
>>> coarse = P.problem(c, f).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions().solve()
>>> fine = P.refine(coarse, cost)
>>> fine.shape
(60, 90)
Multi-action planning¶
Which units to monitor and which actions to take against which threats in them, so that the features recover (or are kept) to their targets at the least cost, or recover the most within a budget: prioriactions's models (Salgado-Rojas et al. 2020), bit for bit, from tables or rasters.
actions_data
¶
actions_data(
pu: DataFrame,
features: DataFrame,
dist_features: DataFrame,
threats: DataFrame,
dist_threats: DataFrame,
sensitivity: DataFrame | None = None,
boundary: DataFrame | None = None,
) -> ActionsData
The inputs of a multi-action problem, checked and completed as prioriactions's inputData().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pu
|
DataFrame
|
Planning units: |
required |
features
|
DataFrame
|
|
required |
dist_features
|
DataFrame
|
The features' amounts in the units: |
required |
threats
|
DataFrame
|
|
required |
dist_threats
|
DataFrame
|
The threats in the units and the actions against them: |
required |
sensitivity
|
DataFrame | None
|
Which features each threat harms: |
None
|
boundary
|
DataFrame | None
|
|
None
|
Notes
Rows with amount 0 are dropped; features without units, threats without units and threats that harm
no feature are dropped with a warning; numbers are rounded to 3 decimals (R's round). With no
status in dist_threats the actions are free (prioriactions resets the units' status there
instead), and the actions in locked-out units are locked out.
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> pu = pd.DataFrame({"id": [1, 2, 3, 4], "monitoring_cost": [2.0, 1.0, 3.0, 1.0]})
>>> features = pd.DataFrame({"id": [1, 2], "target_recovery": [1.0, 0.5]})
>>> dist_features = pd.DataFrame({"pu": [1, 2, 3, 3, 4], "feature": [1, 1, 1, 2, 2], "amount": 1.0})
>>> threats = pd.DataFrame({"id": [1]})
>>> dist_threats = pd.DataFrame({"pu": [1, 2, 3, 4], "threat": 1, "amount": 1.0, "action_cost": [1.0, 4.0, 2.0, 1.0]})
>>> d = ls.prioritize.actions_data(pu, features, dist_features, threats, dist_threats)
>>> sol = d.problem("minimizeCosts").solve()
>>> sol.objective
5.0
>>> sol.action_table()
pu threat solution
0 1 1 1.0
1 2 1 0.0
2 3 1 0.0
3 4 1 1.0
actions_raster_data
¶
actions_raster_data(
monitoring_cost: DataArray,
features,
threats,
action_cost=1.0,
*,
target_recovery=0.0,
target_conservation=0.0,
relative: bool = True,
sensitivity: DataFrame | None = None,
blm_actions=0.0,
locked_in=None,
locked_out=None,
boundary: bool = False,
n_threads: int | None = None,
) -> ActionsData
The inputs of a multi-action problem from rasters (see actions_data() for the tables).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
monitoring_cost
|
DataArray
|
The cost of monitoring every cell: the planning units are the cells with a cost (not NaN), in the order of the cells (ids 1, 2, ...). |
required |
features
|
The features' amounts: a stack (band, y, x), a Dataset or a list of rasters on the same grid (the layers' names are the features' names; NaN = 0). |
required | |
threats
|
The threats' intensity, the same way: a threat occurs in a cell with a value > 0, and its action there removes it. |
required | |
action_cost
|
The cost of acting against every threat: a number, a raster, or one of them per threat (a sequence in the threats' order, a mapping by name or a stack); it needs a value wherever the threat occurs. |
1.0
|
|
target_recovery
|
The features' targets: a number, a sequence in the features' order or a mapping by name. With
|
0.0
|
|
target_conservation
|
The features' targets: a number, a sequence in the features' order or a mapping by name. With
|
0.0
|
|
sensitivity
|
DataFrame | None
|
Which features each threat harms ( |
None
|
blm_actions
|
The weight of the boundary of every threat's actions (a number, sequence or mapping). |
0.0
|
|
locked_in
|
Rasters of the cells that must (or must not) be monitored (non-zero, not NaN). |
None
|
|
locked_out
|
Rasters of the cells that must (or must not) be monitored (non-zero, not NaN). |
None
|
|
boundary
|
bool
|
Give the problem the cells' shared edges as boundary data (for |
False
|
Returns:
| Type | Description |
|---|---|
ActionsData
|
With |
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> threats = ls.load_raster(P.example_data("threats"))
>>> d = P.actions_raster_data(cost, features, threats, action_cost=[20, 5], target_recovery=0.2)
>>> d.n_units, d.n_actions
(4825, 2838)
>>> sol = d.problem("minimizeCosts").add_highs_solver(gap=0.01).solve()
>>> sol.status
'optimal'
>>> [str(b) for b in sol.maps().band.values]
['monitoring', 'deforestation', 'fire']
ActionsData
dataclass
¶
ActionsData(
pu: DataFrame,
features: DataFrame,
dist_features: DataFrame,
threats: DataFrame,
dist_threats: DataFrame,
sensitivity: DataFrame,
boundary: DataFrame | None = None,
grid: object = None,
)
Checked inputs of a multi-action problem (prioriactions's inputData()): see actions_data().
The tables keep their columns plus internal_id (pu, features, threats: positions from 0) and
internal_pu, internal_feature, internal_threat, internal_id1, internal_id2.
problem
¶
problem(
model: str = "minimizeCosts",
budget: float = 0.0,
blm: float = 0.0,
curve: int = 1,
segments: int = 3,
) -> ActionsProblem
The optimization problem (prioriactions's problem()).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str
|
|
'minimizeCosts'
|
budget
|
float
|
The most the monitoring and the actions may cost ( |
0.0
|
blm
|
float
|
The weight of the selected units' boundary (needs boundary data); every threat's
|
0.0
|
curve
|
int
|
The features' response to the actions: 1 linear, 2 quadratic, 3 cubic (benefit = b^curve, needs threat amounts of 0 or 1). |
1
|
segments
|
int
|
Pieces of the curve's linear approximation (1 to 5): pieces of length |
3
|
potential_benefit
¶
Every feature's distribution (units, threatened units) and the most benefit the actions can give
it: conservation (amount in the units without its threats), recovery and total (prioriactions's
getPotentialBenefit(), the conservation benefit as the amount).
ActionsProblem
dataclass
¶
ActionsProblem(
data: ActionsData,
model: str = "minimizeCosts",
budget: float = 0.0,
blm: float = 0.0,
curve: int = 1,
segments: int = 3,
solver: tuple = ("highs", {"gap": 0.0}),
dependencies: tuple = (),
portfolio: tuple | None = None,
)
A multi-action problem (prioriactions's problem()): see problem().
compile
¶
compile() -> OptimizationProblem
prioriactions's model (getModelInfo()'s matrices), bit for bit; with curve 2 or 3 the
power constraints are not in it (Gurobi's general constraints in prioriactions).
add_directional_dependencies
¶
add_directional_dependencies(
flow: DataFrame, penalty: float = 1.0, hard: bool = False
) -> ActionsProblem
Dependencies between planning units, such as upstream and downstream along a river (an
extension of prioriactions, after prioripy): monitoring a target_pu without its
source_pu adds strength x penalty to the cost (or takes it from the benefit), or,
with hard, is not allowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flow
|
DataFrame
|
|
required |
penalty
|
float
|
Multiplies every dependency's strength. |
1.0
|
hard
|
bool
|
Make the dependencies constraints: a target is monitored only with its source. |
False
|
add_cuts_portfolio
¶
add_cuts_portfolio(number_solutions: int = 10) -> ActionsProblem
The best number_solutions distinct solutions (differing in a unit monitored or an action
taken), one solve after another, each forbidding the earlier ones.
add_top_portfolio
¶
add_top_portfolio(number_solutions: int = 10) -> ActionsProblem
As add_cuts_portfolio(), every solve to optimality (gap 0).
add_gap_portfolio
¶
add_gap_portfolio(number_solutions: int = 10, pool_gap: float = 0.1) -> ActionsProblem
Up to number_solutions distinct solutions within pool_gap (relative) of the optimum.
add_shuffle_portfolio
¶
add_shuffle_portfolio(
number_solutions: int = 10, remove_duplicates: bool = True, seed: int | None = None
) -> ActionsProblem
number_solutions solves of the model with its units and actions in random orders.
add_highs_solver
¶
add_highs_solver(
gap: float = 0.0,
time_limit: float | None = None,
n_threads: int | None = None,
verbose: bool = False,
engine: bool = True,
) -> ActionsProblem
Solve on HiGHS to a relative gap (prioriactions's gap_limit, default 0), behind
landschaft's closure engine (engine=False: HiGHS's MIP alone; see solve()).
add_gurobi_solver
¶
add_gurobi_solver(
gap: float = 0.0,
time_limit: float | None = None,
n_threads: int | None = None,
verbose: bool = False,
) -> ActionsProblem
Solve on Gurobi (needs gurobipy and a licence).
solve
¶
Solve the problem.
On HiGHS (the default), the equivalent model (activation one row per action, the benefit
columns substituted) first goes to landschaft's closure engine (a Lagrangian bound as strong as
the linear relaxation, by a closed form or minimum cuts, and repaired selections), then, while
the gap is wider than asked, to HiGHS's MIP from its solution with the units and actions the
bound proves fixed. start: a solution to start from (an ActionsSolution or the
units then the actions, 0/1). With a portfolio, an ActionsPortfolio (solved on HiGHS's
MIP or Gurobi: the no-good cuts do not fit the engine).
ActionsSolution
dataclass
¶
ActionsSolution(
data: ActionsData,
units: ndarray,
actions: ndarray,
local_benefit: DataFrame,
objective: float = nan,
bound: float = nan,
gap: float = nan,
status: str = "",
solver: str = "",
seconds: float = 0.0,
blm: float = 0.0,
x: ndarray = None,
dependencies: tuple = (),
)
Bases: Saveable
A solution of an ActionsProblem.
Attributes:
| Name | Type | Description |
|---|---|---|
units, actions |
The monitored units (0/1, in the order of |
|
local_benefit |
DataFrame
|
Every (unit, feature)'s benefit, by conservation or recovery ( |
objective, bound, gap, status, solver, seconds |
As |
maps
¶
The monitored cells and every threat's actions as rasters (band monitoring, then the threats'
names; NaN outside the planning units), for problems from actions_raster_data().
dependency_penalty
¶
The directional dependencies' penalty: the weights of the targets monitored without their sources (0 without soft dependencies).
connectivity_penalty
¶
The boundary of the monitored units and of every threat's actions (getConnectivityPenalty();
None without boundary data).
action_summary
¶
Every unit's actions (by threat id), whether it conserves a feature without actions
(conservation) and whether it is monitored only for connectivity (connectivity):
prioriactions's getActions(). format="long": one row per (pu, action).
benefit
¶
Every feature's benefit, by conservation and recovery (getSolutionBenefit(); the
conservation benefit as the amount in the monitored units).
ActionsPortfolio
dataclass
¶
Bases: Saveable
Several solutions of an ActionsProblem (add_cuts_portfolio() and the
others).
Attributes:
| Name | Type | Description |
|---|---|---|
solutions |
list
|
The |
summary |
DataFrame
|
Objective, cost (monitoring and actions), status and gap of every solution. |
frequency |
dict
|
The share of the solutions that monitor every unit and take every action: |
maps
¶
The frequencies as rasters (band monitoring, then the threats), for problems from
actions_raster_data().
Restoration planning¶
How much of every converted cell to restore, for the most reduction of the species' extinction risk
(and of the ecosystems' risk of collapse), carbon and ecological intactness within area targets and a
budget, or the cheapest plan for a benefit: the restoration problem of plangea (Strassburg et al. 2020),
with its conservation (conservable=) and conversion (convertible=) forms. The species-area curves are
concave, so the optimum is found and proven, by simplicial decomposition (linear programs over the cells,
mixed by a small concave problem), from thousands of cells to millions; plangea's own procedure (benefit
per cost per cell, the target in steps) is method="steps". Targets per region come from projections of
land demand (add_projected_change), shares of the land (relative=True), other values
(add_linear_target) or the ecosystems' risk of collapse (ecosystem_targets); tradeoff gives the plans
of a growing target and the gradient of priorities, compare_weights ranks sets of weights. The inputs
come from ls.original_vegetation, ls.sdm.area_of_habitat and ls.intactness. Tutorial:
restoration priorities.
restoration_problem
¶
restoration_problem(
restorable=None,
*,
conservable=None,
convertible=None,
limit=1.0,
threat=1.0,
cell_area=None,
) -> RestorationProblem
A restoration problem: how much of every converted cell to restore (and of natural land to conserve or convert).
The planning units are the cells with land an action can take; the decision is the share x_i of
each, from 0 to the share that can be times limit. Then add benefits
(add_extinction_risk_benefit(),
add_linear_benefit(), add_intactness_benefit()),
targets (add_area_target()), a budget, and solve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
restorable
|
The share of every cell that can be restored: a raster, or a
|
None
|
|
conservable
|
The share of every cell's natural land that conservation can keep (plangea's |
None
|
|
convertible
|
The share of every cell's natural land that can be converted (plangea's |
None
|
|
limit
|
plangea's bound limit: a number or a raster multiplying the shares, or a mapping action -> limit. |
1.0
|
|
threat
|
The share of the conservable land lost without conservation, in [0, 1]: a number or a raster (default 1, plangea's). |
1.0
|
|
cell_area
|
The cells' area in km²: a number, a raster, or None (from the grid, see
|
None
|
Returns:
| Type | Description |
|---|---|
RestorationProblem
|
|
Examples:
>>> import numpy as np
>>> import landschaft as ls
>>> rng = np.random.default_rng(1)
>>> anthropic = rng.random((20, 30)) * 0.6
>>> carbon = rng.random((20, 30)) * 100
>>> p = (ls.prioritize.restoration_problem(anthropic, cell_area=1.0)
... .add_linear_benefit(carbon, name="carbon")
... .add_area_target(50.0))
>>> sol = p.solve()
>>> round(sol.area, 6), sol.status
(50.0, 'optimal')
RestorationProblem
dataclass
¶
RestorationProblem(
template: DataArray | None,
shape: tuple,
index: ndarray,
upper: ndarray,
area: ndarray,
benefits: tuple = (),
targets: tuple = (),
cost: ndarray | None = None,
cost_layer: ndarray | None = None,
budget: float | None = None,
objective: tuple = ("max_benefit",),
solver: tuple = ("highs", {}),
action: ndarray | None = None,
natural: ndarray | None = None,
threat: ndarray | None = None,
lower: ndarray | None = None,
)
A restoration problem (see restoration_problem()), built up by its add_* methods; every
method returns a new problem.
add_extinction_risk_benefit
¶
add_extinction_risk_benefit(
areas,
z=PLANGEA_Z,
weight: float = 1.0,
restoration_discount=0.0,
name: str | None = None,
) -> RestorationProblem
The reduction of the species' extinction risk (or of ecosystems' risk of collapse).
A species with habitat A of a potential P has the risk 1 - (A / P)^z (0 at A >= P); restoring a
share x_i of a cell gives it g_i x_i more habitat, g the gain of
area_of_habitat() (or of ecosystem_areas()). Conserving a share of a
cell's natural land keeps the habitat that would be lost (its loss times the threat);
converting it takes that habitat away. The benefit is the sum over the species of the risk
without the plan minus the risk with it (negative when conversion raises the risk).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
areas
|
A |
required | |
z
|
The exponent of the species-area relationship, in (0, 1): a number for every species
(default 0.25, plangea's), one per species (in the order of |
PLANGEA_Z
|
|
weight
|
float
|
The benefit's weight in the objective. |
1.0
|
restoration_discount
|
The share of the habitat value that restored land does NOT deliver, in [0, 1] (0: restored
land is habitat in full, as Strassburg et al. 2020 assume): the gain becomes
g (1 - discount). A number, one per species (in the order of |
0.0
|
|
name
|
str | None
|
The benefit's name (default |
None
|
add_linear_benefit
¶
add_linear_benefit(
values, weight: float = 1.0, name: str | None = None, per: str = "area"
) -> RestorationProblem
A benefit proportional to the area acted on, such as carbon.
values: a raster on the problem's grid, a number or one value per unit, or a mapping action
-> values (0 for the actions left out); per km² (per="area") or per cell in full
(per="cell") of the action. A loss is negative (the carbon stock of converted land, say);
conservation's values count the land the plan saves from loss (they are multiplied by the
threat).
add_intactness_benefit
¶
add_intactness_benefit(
hfi,
reduction=None,
radius: int = 3,
weight: float = 1.0,
*,
increase=None,
gamma: float | None = None,
beta: float = -0.2,
z: float = 0.5,
cell_size=None,
form: str = "linear",
name: str | None = None,
) -> RestorationProblem
Ecological intactness (Beyer et al. 2020): restoring a share x of a cell lowers its human
footprint by reduction x, converting it raises the footprint by increase x (and natural
land lost for want of conservation by increase times the threat, which conserving spares);
the benefit is the total intactness gained. hfi, reduction and increase on the
problem's grid (or numbers); the other parameters as intactness().
form="linear": linearised at the plan of nothing (intactness_gradient();
with method="steps" it is recomputed at every step, as plangea does). form="exact": the
total intactness itself, which is not concave (a sum of exponentials of the footprint): the exact
method improves the plan by successive concave minorants (the tangent of every pair's
exponential, which never overestimates; each step solved exactly, the benefit never falls) to a
local optimum, and bounds the best plan by the secant of every pair over the footprints the
plans can reach. The status is "optimal" when the two meet within the tolerance, else
"local" (the bound is reported). The weight must be at least 0.
add_area_target
¶
add_area_target(
target,
sense: str = "<=",
regions=None,
action: str | None = None,
relative: bool = False,
) -> RestorationProblem
The area acted on (km²): in the whole landscape, or in every region.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
A number (km²), or with |
required | |
sense
|
str
|
|
'<='
|
regions
|
A raster of region codes on the problem's grid (NaN: no region) or one code per unit. |
None
|
|
action
|
str | None
|
The area of |
None
|
relative
|
bool
|
The targets are shares (0-1) of the area that can be acted on (in the landscape or in every region): plangea's flat targets. |
False
|
add_linear_target
¶
add_linear_target(
values,
target,
sense: str = "<=",
regions=None,
per: str = "area",
relative: bool = False,
) -> RestorationProblem
A target on a value other than the area (production, opportunity cost, carbon ...): the sum of
values over the plan, in the landscape or in every region.
values: a raster, a number, one value per unit or a mapping action -> values; per km²
(per="area") or per cell in full (per="cell") of the action. target, sense,
regions and relative as add_area_target() (plangea's targets of other types).
add_projected_change
¶
add_projected_change(
change,
regions,
sense: str = "=",
restrict: bool = True,
compensate: bool | None = None,
) -> RestorationProblem
Projected changes of natural land per region (plangea's econometric targets): positive, land spared for restoration; negative, a demand for land to convert.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
change
|
A mapping region -> change (km²) or a table with columns |
required | |
regions
|
A raster of region codes on the problem's grid or one code per unit. |
required | |
sense
|
str
|
Of every region's row ( |
'='
|
restrict
|
bool
|
A plan that restores and converts acts in every region in the projected direction only:
restoring where land is spared, converting where it is demanded, neither where the change
is 0 (what plangea's |
True
|
compensate
|
bool | None
|
A plan that only restores cannot convert: with |
None
|
add_budget
¶
add_budget(budget: float, cost) -> RestorationProblem
Spend at most budget, cost per km² acted on (a raster, a number, one value per unit
or a mapping action -> cost; the opportunity cost of the land, say).
add_max_benefit_objective
¶
add_max_benefit_objective() -> RestorationProblem
The most weighted benefit within the targets and the budget (the default).
add_min_cost_objective
¶
add_min_cost_objective(benefit: float, cost=None) -> RestorationProblem
The cheapest plan whose weighted benefit reaches benefit; cost per km² acted on (or the
budget's, without it).
add_highs_solver
¶
add_highs_solver(
tolerance: float = 1e-07,
time_limit: float | None = None,
n_threads: int | None = None,
verbose: bool = False,
) -> RestorationProblem
Solve the linear programs on HiGHS (built in). tolerance: the relative gap between the
bound and the plan's benefit (or cost) at which the exact method stops.
add_gurobi_solver
¶
add_gurobi_solver(
tolerance: float = 1e-07,
time_limit: float | None = None,
n_threads: int | None = None,
verbose: bool = False,
) -> RestorationProblem
Solve the linear programs on Gurobi (needs gurobipy and a licence).
solve
¶
solve(method: str = 'exact', steps: int = 1) -> RestorationSolution
Solve the problem.
method="exact": the optimum within the solver's tolerance (relative, between the plan's
benefit and a bound on the best one). Simplicial decomposition: every vertex of the set of plans
is the solution of a linear program over the units with the gradient of the benefits as its
objective and only the targets and the budget as rows; the plan is the best mix of the vertices
found (a small concave problem, by SLSQP); the bound is F(x) + grad F(x) . (v - x), v the
next vertex (valid because the benefits are concave). The minimum cost: a bisection on the
budget, each step the most benefit for it; the bound is the largest budget proven too small.
method="steps": plangea's procedure, for comparison: every cell scored by its weighted
benefits (each layer scaled by 1 / 10^trunc(log10(max))) per cost, the area targets met in
steps equal parts, the extinction-risk and intactness layers recomputed after every part.
It is a heuristic: its benefit is at most the exact one, and depends on steps. Area
targets only (no budget, no minimum cost).
evaluate
¶
evaluate(
plan=None, *, restored=None, conserved=None, converted=None
) -> RestorationSolution
The benefits, cost and area of a plan: the shares of every column (plan: a raster on the
grid or one value per unit when the problem has one action, or one value per column), or the
shares restored, conserved and converted (rasters or one value per unit; 0 for those
left out).
uniform
¶
uniform(area: float | None = None) -> RestorationSolution
plangea's uniform scenario: every unit acted on by the same share of what it can be, for a
total area (default: the first landscape area target). For a plan of one action.
tradeoff
¶
tradeoff(
levels=(0.25, 0.5, 0.75, 1.0),
*,
nested: bool = True,
targets: str = "landscape",
method: str = "exact",
steps: int = 1,
threshold: float = 0.0,
) -> RestorationTradeOff
The plans for a growing target: the trade-off curve, and the priority of every cell.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
levels
|
Shares of the targets (in (0, 1], increasing): plangea's gradient steps. |
(0.25, 0.5, 0.75, 1.0)
|
|
nested
|
bool
|
Every level's plan contains the previous one (a priority order: the exact best plan given the earlier levels, with their shares as the least shares), or (False) every level is the exact optimum on its own (the frontier: plans need not contain each other). |
True
|
targets
|
str
|
The target rows that grow: the landscape's ( |
'landscape'
|
method
|
str
|
|
'exact'
|
threshold
|
float
|
A cell counts in a level when its share is above it (the gradient). |
0.0
|
Returns:
| Type | Description |
|---|---|
RestorationTradeOff
|
The table (one row per level: the target, area, cost, benefits), the plans and the gradient
(plangea's |
compare_weights
¶
compare_weights(
weights, *, method: str = "exact", steps: int = 1
) -> RestorationTradeOff
Solve with every set of the benefits' weights and rank the sets by plangea's metric
(post_choose_weights): every benefit's value (and the cost, inverted) rescaled to 0-1 over
the sets, the mean over them divided by their standard deviation (see mean_sd()); the
best set does well on every benefit at once.
weights: a list of mappings benefit name -> weight (the others keep theirs). Returns the
table (w.<benefit>, the values, cost, mean, sd, score, rank) and the
plans; best() is the first in rank.
RestorationSolution
dataclass
¶
RestorationSolution(
restored: DataArray | None,
values: ndarray,
area: float = 0.0,
cost: float = nan,
benefits: DataFrame = None,
benefit: float = 0.0,
objective: float = nan,
bound: float = nan,
status: str = "",
method: str = "",
rounds: int = 0,
seconds: float = 0.0,
conserved: DataArray | None = None,
converted: DataArray | None = None,
areas: dict = dict(),
)
Bases: Saveable
A restoration plan.
Attributes:
| Name | Type | Description |
|---|---|---|
restored, conserved, converted |
The share of every cell restored, conserved and converted (NaN outside the planning units; None for an action the problem does not have). |
|
values |
ndarray
|
The shares, one value per column (the units of every action, block after block: restore, conserve, convert). |
area, areas |
The area acted on (km²) and the area of every action. |
|
cost |
float
|
Its cost (NaN without a cost). |
benefits |
DataFrame
|
One row per benefit: its value, weight and weighted value. |
benefit |
float
|
The weighted benefit. |
objective, bound |
The objective (the weighted benefit, or the cost with the minimum-cost objective) and the bound
on the optimum the exact method proved (NaN for |
|
status, method, rounds, seconds |
|
RestorationTradeOff
dataclass
¶
Bases: Saveable
Plans over a trade-off (tradeoff()) or over sets of weights
(compare_weights()).
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
One row per plan. |
solutions |
list
|
The |
gradients |
dict
|
The trade-off's gradient per action (an empty mapping for weights). |
best
¶
best() -> RestorationSolution
The plan ranked first (compare_weights), or the knee of the curve of benefit against area.
ecosystem_areas
¶
The natural area of every ecosystem (ecoregion) now and before conversion, and what restoring a
cell gives it: the input of add_extinction_risk_benefit() for the risk of
ecosystem collapse (plangea's ec).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
land_cover
|
The share of every class in each cell ( |
required | |
natural
|
The natural and the anthropic classes. |
required | |
anthropic
|
The natural and the anthropic classes. |
required | |
ecoregions
|
A raster of ecoregion codes (NaN: none). |
required | |
cell_area
|
As |
None
|
Returns:
| Type | Description |
|---|---|
HabitatAreas
|
One row per ecoregion ( |
Notes
plangea sums the natural area of an ecoregion only over the cells of its problem (those with anthropic land), which leaves out the ecoregion's untouched land and overstates its loss; here every cell of the ecoregion counts.
Examples:
>>> import numpy as np
>>> import xarray as xr
>>> import landschaft as ls
>>> cover = xr.DataArray(np.array([[[0.8, 0.2, 0.0, 0.5]], [[0.2, 0.8, 1.0, 0.5]]]),
... dims=("band", "y", "x"), coords={"band": ["forest", "crops"]})
>>> eco = np.array([[1.0, 1.0, 2.0, 2.0]])
>>> ea = ls.prioritize.ecosystem_areas(cover, ["forest"], ["crops"], eco, cell_area=1.0)
>>> ea.table[["species", "current", "potential"]]
species current potential
0 1 1.0 2.0
1 2 0.5 2.0
ecosystem_targets
¶
ecosystem_targets(
areas,
target: float,
*,
minimum: float = 0.0,
z: float = PLANGEA_Z,
half_life: float = 0.0,
sense: str = "<=",
method: str = "exact",
steps: int = 10,
) -> DataFrame
Share a restoration target among ecosystems for the least risk of collapse (plangea's
ecosystemic sub-region targets, harmonize_sr_ecosys).
Every ecosystem e with natural area N_e of a potential T_e (ecosystem_areas()) gets a_e of
the target, between its least share and its converted area T_e - N_e, for the most total of
((N_e + a_e) / T_e)^z, each ecosystem's marginal value damped by exp(half_life (N_e + a_e) /
T_e) (plangea's "yukawa" factor; 0: none).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
areas
|
A |
required | |
target
|
float
|
The area to share (km²). |
required |
minimum
|
float
|
Every ecosystem's least share, as a part (0-1) of an equal split of the target (plangea's
|
0.0
|
z
|
float
|
The exponent of the species-area relationship. |
PLANGEA_Z
|
half_life
|
float
|
The damping exponent (below 1 - z, so the marginal value falls with the area). |
0.0
|
sense
|
str
|
The sense of the targets returned (plangea's |
'<='
|
method
|
str
|
|
'exact'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> import numpy as np
>>> import xarray as xr
>>> import landschaft as ls
>>> cover = xr.DataArray(np.array([[[0.8, 0.2, 0.1, 0.5]], [[0.2, 0.8, 0.9, 0.5]]]),
... dims=("band", "y", "x"), coords={"band": ["forest", "crops"]})
>>> eco = np.array([[1.0, 1.0, 2.0, 2.0]])
>>> ea = ls.prioritize.ecosystem_areas(cover, ["forest"], ["crops"], eco, cell_area=1.0)
>>> ls.prioritize.ecosystem_targets(ea, 1.0).round(4)
region target sense
0 1.0 0.3 <=
1 2.0 0.7 <=
eval_risk_reduction
¶
eval_risk_reduction(
problem: RestorationProblem,
solution,
z_sd: float | None = None,
*,
benefit: str | None = None,
method: str = "correlated",
step: float = 1e-06,
) -> RiskReduction
The reduction of the extinction risk a plan brings (plangea's multi_exrisk_aggregate), with
the uncertainty of the species-area exponent z.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
RestorationProblem
|
The restoration problem. |
required |
solution
|
A |
required | |
z_sd
|
float | None
|
The standard deviation of z (plangea: 0.1): a number for every species, one per species (in the order of the benefit's species) or a mapping species -> deviation (0 for the others); None: no uncertainty. |
None
|
benefit
|
str | None
|
The extinction-risk benefit to evaluate (default: the first). |
None
|
method
|
str
|
How the species' sensitivities to z add up. |
'correlated'
|
step
|
float
|
The forward difference of z (plangea's 1e-6). |
1e-06
|
Returns:
| Type | Description |
|---|---|
RiskReduction
|
|
Examples:
>>> import numpy as np
>>> import xarray as xr
>>> import landschaft as ls
>>> cover = xr.DataArray(np.array([[[0.6, 0.0, 0.3]], [[0.2, 0.1, 0.0]], [[0.2, 0.9, 0.7]]]),
... dims=("band", "y", "x"), coords={"band": ["forest", "savanna", "crops"]})
>>> ranges = xr.DataArray(np.ones((2, 1, 3)), dims=("band", "y", "x"), coords={"band": ["toucan", "rhea"]})
>>> veg = ls.original_vegetation(cover, ["forest", "savanna"], ["crops"])
>>> aoh = ls.sdm.area_of_habitat(ranges, cover, ["forest", "savanna"], original=veg, cell_area=1.0,
... suitability={"toucan": ["forest"], "rhea": ["savanna"]})
>>> p = (ls.prioritize.restoration_problem(veg, cell_area=1.0)
... .add_extinction_risk_benefit(aoh).add_area_target(0.8))
>>> sol = p.solve()
>>> rr = ls.prioritize.eval_risk_reduction(p, sol, z_sd=0.1)
>>> round(rr.total, 4), round(rr.sd, 4)
(0.269, 0.0871)
RiskReduction
dataclass
¶
Bases: Saveable
Result of eval_risk_reduction(): per species, its habitat now (current), without the
plan (baseline: less than now when conservation's baseline loses land) and gained by it, its
risk before and after; the total reduction (negative when conversion raises the risk) and its
standard deviation from the uncertainty of z.
Simulated annealing¶
Marxan 4's runs, unit by unit as the program, and its annealing as a solver of any problem
(Problem.add_anneal_solver) for scales where a gap matters less than an answer.
marxan
¶
marxan(
pu: DataFrame,
spec: DataFrame,
puvspr: DataFrame,
bound: DataFrame | None = None,
*,
blm: float = 0.0,
num_reps: int = 10,
num_iters: int = 1000000,
start_temp: float = -1.0,
cool_fac: float = 0.0,
num_temp: int = 10000,
run_mode: int = 1,
heur_type: int = 1,
itimp_type: int = 0,
prop: float = 0.0,
misslevel: float = 1.0,
cost_thresh: float = 0.0,
thresh_pen1: float = 0.0,
thresh_pen2: float = 0.0,
connectivity_in: bool = False,
seed: int = 1,
n_threads: int | None = None,
) -> MarxanResult
Run Marxan: num_reps independent runs of annealing and iterative improvement (or the
greedy heuristics) minimising cost + blm x boundary + penalties for the features' shortfalls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pu
|
DataFrame
|
Planning units: |
required |
spec
|
DataFrame
|
Features: |
required |
puvspr
|
DataFrame
|
Amount of every feature in every planning unit: |
required |
bound
|
DataFrame | None
|
Boundaries: |
None
|
blm
|
float
|
|
0.0
|
num_reps
|
float
|
|
0.0
|
num_iters
|
float
|
|
0.0
|
start_temp
|
float
|
|
0.0
|
cool_fac
|
float
|
|
0.0
|
num_temp
|
float
|
|
0.0
|
run_mode
|
float
|
|
0.0
|
heur_type
|
float
|
|
0.0
|
itimp_type
|
float
|
|
0.0
|
prop
|
float
|
|
0.0
|
misslevel
|
float
|
Marxan's |
1.0
|
cost_thresh
|
float
|
Marxan's |
1.0
|
thresh_pen1
|
float
|
Marxan's |
1.0
|
thresh_pen2
|
float
|
Marxan's |
1.0
|
connectivity_in
|
bool
|
Marxan's CONNECTIVITYIN: count the boundaries inside the reserve instead of at its edge. |
False
|
seed
|
int
|
RANDSEED: the same seed gives the same runs on any number of threads. |
1
|
n_threads
|
int | None
|
Runs in parallel (default: all cores but one). |
None
|
Returns:
| Type | Description |
|---|---|
MarxanResult
|
|
Examples:
>>> import landschaft as ls
>>> P = ls.prioritize
>>> cost = ls.load_raster(P.example_data("cost"))
>>> features = ls.load_raster(P.example_data("features"))
>>> p = P.problem(cost, features).add_min_set_objective().add_relative_targets(0.2).add_binary_decisions()
>>> pu = p.units
>>> import numpy as np, pandas as pd
>>> rij = pu.rij.tocoo()
>>> res = P.marxan(pd.DataFrame({"id": np.arange(1, pu.n + 1), "cost": pu.cost, "status": 0}),
... pd.DataFrame({"id": np.arange(1, 9), "prop": 0.2, "spf": 10.0}),
... pd.DataFrame({"species": rij.row + 1, "pu": rij.col + 1, "amount": rij.data}),
... num_reps=4, num_iters=20000, seed=1)
>>> len(res.summary)
4
>>> list(res.solutions.columns[:3])
['planning_unit', 'solution_1', 'solution_2']
MarxanResult
dataclass
¶
MarxanResult(
solutions: DataFrame,
best: DataFrame,
summary: DataFrame,
penalties: DataFrame,
frequency: DataFrame,
best_run: int = 1,
unmet: int = 0,
)
Bases: Saveable
The runs of marxan().
Attributes:
| Name | Type | Description |
|---|---|---|
solutions |
DataFrame
|
Planning units ( |
best |
DataFrame
|
The best run's selection ( |
summary |
DataFrame
|
Every run's figures, as Marxan's |
penalties |
DataFrame
|
Every feature's |
frequency |
DataFrame
|
|
best_run |
int
|
The best run's number (from 1). |