Ecosystem services¶
Models of InVEST
(the Natural Capital Project's natcap.invest, Apache-2.0), reimplemented in C++
from land use maps and parameter tables: their maps, bit for bit where the
arithmetic permits. A simulated land use map (ls.simulate) or a series of
scenarios goes straight in.
Carbon storage and sequestration¶
carbon
¶
carbon(
lulc,
pools,
*,
future=None,
price: float | None = None,
discount_rate: float | None = None,
rate_change: float = 0.0,
year: int | None = None,
future_year: int | None = None,
nodata=None,
n_threads: int | None = None,
) -> Carbon
Carbon storage of a landscape and its change between two maps (InVEST's carbon model).
Each land use class holds a density of carbon (tons per hectare) in four pools: aboveground and belowground biomass, soil and dead organic matter. The storage of a map is the sum of its pools cell by cell; with a future map, the change is the sequestration (future minus baseline), and with a price, its net present value.
The maps are those of InVEST's carbon model (natcap.invest 3.20), bit for
bit: float32 densities, the pools summed in their order, the change and its
value in float32; the landscape totals agree to the last digits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lulc
|
The baseline land use map (integer codes; a projected raster in metres). |
required | |
pools
|
The densities by class: a DataFrame with |
required | |
future
|
The alternate (future) land use map, on the same grid. |
None
|
|
price
|
float | None
|
Valuation of the change (with |
None
|
discount_rate
|
float | None
|
Valuation of the change (with |
None
|
rate_change
|
float | None
|
Valuation of the change (with |
None
|
year
|
int | None
|
The years of the two maps (needed with |
None
|
future_year
|
int | None
|
The years of the two maps (needed with |
None
|
nodata
|
Value marking missing cells in the maps (besides their own nodata). |
None
|
Returns:
| Type | Description |
|---|---|
Carbon
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> from affine import Affine
>>> import landschaft as ls
>>> lulc = ls.write_raster(np.array([[1, 1, 2], [2, 3, 3]]), "lulc.tif", crs="EPSG:31983",
... transform=Affine(30, 0, 500000, 0, -30, 8000000)) # 30 m cells
>>> pools = pd.DataFrame({"lucode": [1, 2, 3], "c_above": [140, 35, 5], "c_below": [40, 10, 2],
... "c_soil": [90, 80, 60], "c_dead": [12, 3, 0]})
>>> later = np.array([[1, 2, 2], [2, 3, 3]])
>>> c = ls.services.carbon(ls.load_raster(lulc), pools, future=later)
>>> c.storage.values[0, 0], c.change.values[0, 1]
(np.float32(282.0), np.float32(-154.0))
>>> c.totals
label total units
0 Baseline Carbon Storage 85.86 t
1 Alternate Carbon Storage 72.00 t
2 Change in Carbon Storage -13.86 t
Carbon
dataclass
¶
Carbon(
storage: DataArray,
pools: dict,
totals: DataFrame,
storage_future: DataArray | None = None,
pools_future: dict = dict(),
change: DataArray | None = None,
npv: DataArray | None = None,
valuation: float | None = None,
)
Bases: Saveable
Carbon stored in a landscape, and its change (carbon()).
Maps are densities in metric tons of carbon per hectare (float32, as InVEST writes them), NaN where the land use map has no value.
Attributes:
| Name | Type | Description |
|---|---|---|
storage |
DataArray
|
Total carbon of the baseline map: the sum of the four pools. |
pools |
dict
|
Each pool's map ( |
storage_future, pools_future |
The same for the alternate (future) map, if given. |
|
change |
DataArray or None
|
Sequestration: future minus baseline storage. |
npv |
DataArray or None
|
Net present value of the change (currency per hectare), with valuation. |
totals |
DataFrame
|
The landscape totals ( |
valuation |
float or None
|
The factor turning a change in carbon into its net present value. |
valuation_constant
¶
valuation_constant(
price: float, discount_rate: float, rate_change: float, year: int, future_year: int
) -> float
The factor turning a change in carbon into its net present value (InVEST's).
The change of storage between the two years, spread evenly over them, valued
at price per ton rising by rate_change percent a year and discounted
at discount_rate percent a year:
price / n * sum_{t=0}^{n-1} 1 / ((1 + d/100) (1 + r/100))^t, n the years.
Examples:
Annual water yield and hydropower¶
water_yield
¶
water_yield(
lulc,
biophysical,
precipitation,
eto,
depth,
pawc,
*,
seasonality: float,
watersheds=None,
subwatersheds=None,
demand=None,
valuation=None,
nodata=None,
n_threads: int | None = None,
) -> WaterYield
Annual water yield, consumption and hydropower value (InVEST's annual water yield model).
The water balance of each cell on the Budyko curve in Fu and Zhang's form:
actual evapotranspiration over precipitation
AET/P = 1 + PET/P - (1 + (PET/P)^w)^(1/w), at most PET/P, with
PET = Kc ET0 and w = Z AWC / P + 1.25 (at most 5),
AWC = min(root depth, soil depth) x PAWC; classes that are not vegetation
(water, wetlands, towns) evaporate min(Kc ET0, P). The yield is
P - AET. With watersheds, the means over each polygon and the yield
volume; with a demand table, the consumption and the realized supply; with
a valuation table, the hydropower energy and its net present value.
The maps are InVEST's (natcap.invest 3.20) bit for bit, in the same float32 (or float64) arithmetic; the watershed means agree to float32 precision (InVEST adds the cells in float32).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lulc
|
Land use map (integer codes; projected, in metres). |
required | |
biophysical
|
A DataFrame by class ( |
required | |
precipitation
|
Annual precipitation and reference evapotranspiration (mm), on the grid of
|
required | |
eto
|
Annual precipitation and reference evapotranspiration (mm), on the grid of
|
required | |
depth
|
Depth to the root restricting layer (mm). |
required | |
pawc
|
Plant available water content (fraction, 0 to 1). |
required | |
seasonality
|
float
|
The Z parameter (about 1 to 30): the seasonal distribution of rain. |
required |
watersheds
|
Polygons (GeoDataFrame) with a |
None
|
|
subwatersheds
|
Polygons (GeoDataFrame) with a |
None
|
|
demand
|
A DataFrame by class ( |
None
|
|
valuation
|
A DataFrame by watershed ( |
None
|
|
nodata
|
Value marking missing cells in the land use map. |
None
|
Returns:
| Type | Description |
|---|---|
WaterYield
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> from affine import Affine
>>> import landschaft as ls
>>> codes = np.array([[1, 1, 2], [1, 2, 3]]) # forest, pasture, water
>>> lulc = ls.load_raster(ls.write_raster(codes, "wy_lulc.tif", crs="EPSG:31983",
... transform=Affine(30, 0, 500000, 0, -30, 8000000)))
>>> bio = pd.DataFrame({"lucode": [1, 2, 3], "lulc_veg": [1, 1, 0], "root_depth": [5000, 1500, 1],
... "kc": [1.0, 0.65, 1.05]})
>>> rain, et0 = lulc.copy(data=np.full((2, 3), 1500.0)), lulc.copy(data=np.full((2, 3), 1200.0))
>>> soil, pawc = lulc.copy(data=np.full((2, 3), 2000.0)), lulc.copy(data=np.full((2, 3), 0.2))
>>> wy = ls.services.water_yield(lulc, bio, rain, et0, soil, pawc, seasonality=5.0)
>>> wy.wyield.values.round(1) # mm per year
array([[582.6, 582.6, 864.3],
[582.6, 864.3, 240. ]])
WaterYield
dataclass
¶
WaterYield(
wyield: DataArray,
aet: DataArray,
pet: DataArray,
fractp: DataArray,
demand: DataArray | None = None,
watersheds: object = None,
subwatersheds: object = None,
)
Bases: Saveable
Annual water yield of a landscape (water_yield()).
Maps are in millimetres per year, NaN where an input has no value (or there is no rain, as InVEST leaves those cells out of the balance); float32, or float64 where InVEST computes in float64 (float64 inputs).
Attributes:
| Name | Type | Description |
|---|---|---|
wyield |
DataArray
|
Water yield: precipitation minus actual evapotranspiration. |
aet |
DataArray
|
Actual evapotranspiration. |
pet |
DataArray
|
Potential evapotranspiration: reference evapotranspiration times Kc. |
fractp |
DataArray
|
Actual evapotranspiration as a fraction of precipitation (float32). |
demand |
DataArray or None
|
Consumptive water use per cell (m3 per year), with a demand table. |
watersheds, subwatersheds |
GeoDataFrame or None
|
The polygons with |
Sediment delivery ratio¶
sdr
¶
sdr(
dem,
erosivity,
erodibility,
lulc,
biophysical,
watersheds,
*,
threshold: float,
k: float = 2.0,
ic_0: float = 0.5,
sdr_max: float = 0.8,
l_max: float = 122.0,
method: str = "mfd",
drainage=None,
nodata=None,
n_threads: int | None = None,
) -> SDR
Sediment export, deposition and retention (InVEST's sediment delivery ratio model).
The soil loss of each cell by the revised universal soil loss equation,
USLE = R K LS C P (the LS factor of Desmet & Govers 1996 from the flow
accumulation), and the share of it that reaches a stream, the sediment
delivery ratio SDR = SDR_max / (1 + exp((IC_0 - IC) / k)) of the index of
connectivity IC = log10(D_up / D_dn) (Borselli et al. 2008):
D_up = W S sqrt(A) upslope (W the C factor, S the slope, averaged over
the contributing area A) and D_dn = sum d / (W S) down to the stream.
The sediment that does not reach a stream is deposited downslope; the
retention is what the vegetation and practices avoid, plus that deposition.
The rasters are InVEST's (natcap.invest 3.20) bit for bit, in its float32 and float64 types: the DEM filled, routed (pygeoprocessing's multiple flow directions or D8) and traced into streams as pygeoprocessing does, its percent slope, and the deposition; the watershed totals agree to float32 precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dem
|
Elevations (projected, in metres). |
required | |
erosivity
|
Rainfall erosivity R (MJ mm / (ha h yr)) and soil erodibility K (t ha h / (ha MJ mm)), on the DEM's grid. |
required | |
erodibility
|
Rainfall erosivity R (MJ mm / (ha h yr)) and soil erodibility K (t ha h / (ha MJ mm)), on the DEM's grid. |
required | |
lulc
|
Land use map (integer codes). |
required | |
biophysical
|
A DataFrame by class ( |
required | |
watersheds
|
Polygons (GeoDataFrame): cells outside them are left out (as InVEST masks the inputs to them), and the totals are given per polygon. |
required | |
threshold
|
float
|
Flow accumulation (cells) from which a cell is a stream. |
required |
k
|
float
|
Borselli's calibration parameters of the SDR. |
2.0
|
ic_0
|
float
|
Borselli's calibration parameters of the SDR. |
2.0
|
sdr_max
|
float
|
Borselli's calibration parameters of the SDR. |
2.0
|
l_max
|
float
|
The largest slope length factor L. |
122.0
|
method
|
str
|
|
'mfd'
|
drainage
|
Cells that drain to a stream otherwise (1), such as roads or ditches. |
None
|
|
nodata
|
Value marking missing cells in the land use map. |
None
|
Returns:
| Type | Description |
|---|---|
SDR
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import geopandas as gpd
>>> from affine import Affine
>>> from shapely.geometry import box
>>> import landschaft as ls
>>> y, x = np.mgrid[:40, :50]
>>> z = (100 + 0.5 * x + 8 * np.sin(y / 5.0)).astype("float32") # a slope with ridges
>>> t = Affine(30, 0, 500000, 0, -30, 8000000)
>>> dem = ls.load_raster(ls.write_raster(z, "sdr_dem.tif", crs="EPSG:31983", transform=t))
>>> lulc = dem.copy(data=np.where(x < 25, 1, 2).astype("int16")) # forest, crops
>>> r, k = dem.copy(data=np.full(z.shape, 4000, "float32")), dem.copy(data=np.full(z.shape, 0.03, "float32"))
>>> bio = pd.DataFrame({"lucode": [1, 2], "usle_c": [0.003, 0.25], "usle_p": [1.0, 0.8]})
>>> ws = gpd.GeoDataFrame({"ws_id": [1]}, geometry=[box(*dem.rio.bounds())], crs=dem.rio.crs)
>>> s = ls.services.sdr(dem, r, k, lulc, bio, ws, threshold=50)
>>> crops, forest = np.nanmean(s.usle.values[:, 30:]), np.nanmean(s.usle.values[:, :20])
>>> bool(crops > 10 * forest) # crops lose far more soil (t / ha / yr)
True
>>> list(s.watersheds.columns)
['ws_id', 'geometry', 'usle_tot', 'sed_export', 'sed_dep', 'avoid_exp', 'avoid_eros']
SDR
dataclass
¶
SDR(
usle: DataArray,
sed_export: DataArray,
sed_deposition: DataArray,
avoided_export: DataArray,
avoided_erosion: DataArray,
rkls: DataArray,
sdr: DataArray,
ic: DataArray,
stream: DataArray,
drains_to_stream: DataArray,
watersheds: object,
intermediate: dict = dict(),
)
Bases: Saveable
Sediment export and retention of a landscape (sdr()).
Maps in tons per hectare and year (float32), NaN where a cell has no value;
usle, rkls and avoided_erosion have none on the streams, as in
InVEST.
Attributes:
| Name | Type | Description |
|---|---|---|
usle |
DataArray
|
Soil loss, RKLS x C x P. |
sed_export |
DataArray
|
The soil loss that reaches the streams: USLE x SDR. |
sed_deposition |
DataArray
|
Sediment from upslope that the cell traps. |
avoided_export |
DataArray
|
Total retention: avoided erosion x SDR plus the deposition. |
avoided_erosion |
DataArray
|
Erosion the vegetation and practices avoid: RKLS - USLE. |
rkls |
DataArray
|
Soil loss of bare soil, R x K x LS. |
sdr |
DataArray
|
Sediment delivery ratio (1 on the streams). |
ic |
DataArray
|
Index of connectivity, log10(D_up / D_dn). |
stream |
DataArray
|
The streams (1), with the drainage given (0 elsewhere). |
drains_to_stream |
DataArray
|
1 where the flow reaches a stream, 0 where it does not (no D_dn). |
watersheds |
GeoDataFrame
|
The watersheds with |
intermediate |
dict
|
The other rasters of the chain ( |
Habitat quality and rarity¶
habitat_quality
¶
habitat_quality(
lulc,
threats,
sensitivity,
threat_maps,
*,
half_saturation: float,
future=None,
future_threat_maps=None,
base=None,
base_threat_maps=None,
access=None,
nodata=None,
n_threads: int | None = None,
) -> HabitatQuality
Habitat degradation, quality and rarity (InVEST's habitat quality model).
Each threat (crops, roads, towns...) degrades the habitat around it: its
impact decays linearly or exponentially with the distance to the nearest
cell of the threat, up to its maximum distance (exponentially: to 5 % there),
and weighs by its relative weight, by the sensitivity of the cell's land use
class to it and by the cell's access (legal protection). The degradation
D of a cell is the sum over the threats; its quality is
h (1 - D^z / (D^z + k^z)), h the habitat suitability of its class, k the
half-saturation constant and z = 2.5. With a base map, the rarity of each
class of the current (and future) map: 1 - n_x / (n_base + n_x), the
cell counts in the two maps.
The maps are InVEST's (natcap.invest 3.20, which takes the distance to the
nearest threat cell since 3.12): the same distance transform in float32 and
the same float32 expressions, bit for bit (the exponential decay through
numpy's float32 exp, as InVEST computes it).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lulc
|
The current land use map (integer codes; projected, in metres). |
required | |
threats
|
A DataFrame with a row per threat: |
required | |
sensitivity
|
A DataFrame by land use class ( |
required | |
threat_maps
|
The current map of each threat, |
required | |
half_saturation
|
float
|
The half-saturation constant k (often half the highest degradation). |
required |
future
|
A future land use map and its threat maps. |
None
|
|
future_threat_maps
|
A future land use map and its threat maps. |
None
|
|
base
|
A base (reference) land use map, for the rarity, and its threat maps (optional: without them the base map has no degradation nor quality). |
None
|
|
base_threat_maps
|
A base (reference) land use map, for the rarity, and its threat maps (optional: without them the base map has no degradation nor quality). |
None
|
|
access
|
Accessibility to threats, from 0 (inaccessible) to 1: a raster, or
polygons (GeoDataFrame) with an |
None
|
|
nodata
|
Value marking missing cells in the land use maps. |
None
|
Returns:
| Type | Description |
|---|---|
HabitatQuality
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> from affine import Affine
>>> import landschaft as ls
>>> lulc = np.ones((40, 40), int)
>>> lulc[:, 20:] = 2 # forest (1), pasture (2)
>>> road = np.zeros((40, 40))
>>> road[:, 30] = 1
>>> lulc = ls.load_raster(ls.write_raster(lulc, "hq_lulc.tif", crs="EPSG:31983",
... transform=Affine(30, 0, 500000, 0, -30, 8000000)))
>>> threats = pd.DataFrame({"threat": ["road"], "max_dist": [600.0], "weight": [1.0],
... "decay": ["linear"]})
>>> sens = pd.DataFrame({"lucode": [1, 2], "habitat": [1.0, 0.0], "road": [0.8, 0.0]})
>>> hq = ls.services.habitat_quality(lulc, threats, sens, {"road": lulc.copy(data=road)},
... half_saturation=0.1)
>>> q = hq.quality["current"].values
>>> q[0, 0] > q[0, 19], q[0, 25] # less quality near the road
(np.True_, np.float32(0.0))
HabitatQuality
dataclass
¶
HabitatQuality(
degradation: dict,
quality: dict,
threats: dict,
rarity: dict = dict(),
rarity_table: DataFrame | None = None,
)
Bases: Saveable
Habitat degradation, quality and rarity (habitat_quality()).
Maps are float32 (as InVEST writes them), NaN where the land use map has no
value. degradation, quality, threats and rarity are dicts by
scenario: "current" and, when given, "future" and "base".
Attributes:
| Name | Type | Description |
|---|---|---|
degradation |
dict
|
Total degradation of each scenario: the sum over the threats. |
quality |
dict
|
Habitat quality: |
threats |
dict
|
Each scenario's degradation by threat (a dict by threat name). |
rarity |
dict
|
Rarity of the current and future map against the base map: by cell
( |
rarity_table |
DataFrame
|
|