spacemodel for Eco-SSL
First step is load the package spacemodR.
You can install from CRAN or from the github repository
The following examples are given for a specific French situation. The methods are designed to be updated for other countries.
For new regions, new layers or new features, feel free to open an issue at this link: issue link.
The get_departements_for_roi function identifies the
codes of French departments that intersect with a given Region of
Interest (ROI). This is particularly useful for spatial analyses in
France, where administrative boundaries (like departments) are often
needed to fetch or process geospatial data.
roi: An sf or sfc object
representing your region of interest. The function uses only the first
geometry (polygon) in the object.The get_ocsge_data function is designed to download,
extract, and prepare OCS GE (Occupation du Sol à Grande Échelle) version
2 datasets for a specific Region of Interest (ROI).
This function automates the process of fetching spatial data from the French National Geographic Institute (IGN), merging data from all relevant departments, and cropping the result to match the boundaries of your ROI.
Input Parameters:
roi: An sf object (simple features)
defining your region of interest. Only the first feature in the object
is used.local_cache (optional): A directory path where
downloaded archives are stored. If not provided, a temporary directory
is used.The function ensures that the ROI is transformed into the Lambert-93 projection (EPSG:2154), which is the standard coordinate system for French geographic data.
## This function is long to compute so we load the data set
# sf_site <- get_ocsge_data(roi_metaleurop)
data(ocsge_metaleurop)You can then plot the object using for instance the
ggplot2 library which is not included in
spacemodR.
library(ggplot2)
ggplot() +
theme_minimal() +
geom_sf(data=ocsge_metaleurop, aes(fill=code_cs)) +
geom_sf(data=roi_metaleurop, fill=NA, color="red", size=1)raster_stack: a stack of raster for ground and
habitats.layer_CS2111 = ocsge_metaleurop[ocsge_metaleurop$code_cs == "CS2.1.1.1", ]
layer_CS221 = ocsge_metaleurop[ocsge_metaleurop$code_cs == "CS2.2.1", ]
layer_CS1121 = ocsge_metaleurop[ocsge_metaleurop$code_cs == "CS1.1.2.1", ]The habitat function creates a spatial habitat object,
which inherits from both sf and data.frame
classes.
This object is designed to represent habitat zones and includes
specific attributes: a habitat column (logical, indicating
the presence or absence of habitat), a weight column
(numeric, representing relative importance or weight), and a
geometry column (containing spatial geometries).
If no geometry is provided, the function creates an empty object. It also performs checks to ensure that the lengths of the habitat and weight vectors match the number of geometries and that their types are correct (logical for habitat, numeric for weight).
This function is useful for initializing spatial analyses where distinguishing between habitat and non-habitat zones is essential.
habitat_11 = habitat(layer_CS2111, weight=0.2)
head(habitat_11)
#> Simple feature collection with 1 feature and 2 fields
#> Geometry type: MULTIPOLYGON
#> Dimension: XY
#> Bounding box: xmin: 697049.4 ymin: 7032094 xmax: 709595.3 ymax: 7043419
#> Projected CRS: RGF93 v1 / Lambert-93
#> habitat weight geometry
#> 1 TRUE 0.2 MULTIPOLYGON (((697891.4 70...habitat_12 = habitat(
layer_CS2111,
habitat=sample(c(TRUE,FALSE), nrow(layer_CS2111), replace=TRUE),
weight=runif(nrow(layer_CS2111), 0, 10)
)
plot(habitat_12)Another option more convenient use of the function for more complex
situation, an idea is the use the function add_habitat.
The add_habitat function is a generic method that adds
habitat zones to an existing habitat object. It takes as input a hab
object of class habitat, an sf object sf_data
containing the geometries to add, and an optional weight parameter
(defaulting to 1) to assign weights to the new geometries. The function
checks that sf_data is indeed an sf object,
then creates a temporary habitat object with the geometries to be added,
marking them as habitat (habitat = TRUE).
If the initial hab object is empty, the new object
becomes the result. Otherwise, the two objects are merged using
dplyr::bind_rows, while preserving the habitat class. This
function is particularly useful for dynamically expanding habitat zones
in a spatial analysis.
habitat_20 = habitat() |>
add_habitat(layer_CS2111)
class(habitat_20)
#> [1] "habitat" "sf" "data.frame"habitat_21 = habitat() |>
add_habitat(layer_CS2111, weight=0.2)
head(habitat_21)
#> Simple feature collection with 1 feature and 2 fields
#> Geometry type: MULTIPOLYGON
#> Dimension: XY
#> Bounding box: xmin: 697049.4 ymin: 7032094 xmax: 709595.3 ymax: 7043419
#> Projected CRS: RGF93 v1 / Lambert-93
#> habitat weight geometry
#> 1 TRUE 0.2 MULTIPOLYGON (((697891.4 70...habitat_22 = habitat() |>
add_habitat(layer_CS2111, weight=0.2) |>
add_habitat(layer_CS1121, weight=0.4) |>
add_nohabitat(layer_CS221)
head(habitat_22)
#> Simple feature collection with 3 features and 2 fields
#> Geometry type: MULTIPOLYGON
#> Dimension: XY
#> Bounding box: xmin: 695704.7 ymin: 7031268 xmax: 709595.3 ymax: 7043983
#> Projected CRS: RGF93 v1 / Lambert-93
#> habitat weight geometry
#> 1 TRUE 0.2 MULTIPOLYGON (((697891.4 70...
#> 2 TRUE 0.4 MULTIPOLYGON (((698440.6 70...
#> 3 FALSE 0.0 MULTIPOLYGON (((698052.4 70...What we consider habitat is defined by the purpose of
the model.
There is 3 situations: 1. the area where the species cannot go, even just passing (impossible)
A raster of
Here, we download a raster layer with the interpolation of the ground concentration of cadmium.
# the raster tif file is internal to the spacemodR package
ground_cd <- load_raster_extdata("ground_concentration_cd_compressed.tif")
terra::plot(ground_cd)Then, it’s the same algorithm to specified habitat and then build rasters.
# prepare a list of OSGE code to make life easier
OCSGE_codes <- list(
forest = c("CS2.1.1.1", "CS2.1.1.2", "CS2.1.1.3", "CS2.1.3"),
grass = "CS2.2.1",
water = "CS1.2.2",
soil = c("CS1.2.1","CS2.1.1.1","CS2.1.1.2","CS2.1.1.3",
"CS2.1.2","CS2.1.3","CS2.2.1","CS2.2.3"),
sealed = c("CS1.1.1.1", "CS1.1.1.2", "CS1.1.2.1")
)layer_soil_natural = ocsge_metaleurop[ocsge_metaleurop$code_cs %in% OCSGE_codes$soil, ]
layer_soil_artificial = ocsge_metaleurop[ocsge_metaleurop$code_cs %in% OCSGE_codes$sealed, ]
habitat_sol = habitat() |>
add_habitat(layer_soil_natural) |>
add_nohabitat(layer_soil_artificial)
plot(habitat_sol)trophic_df: a data.frame of
trophic webYou can use a data.frame like this:
df_raw <- data.frame(
src = c("sol", "sol", "sp10", "sp12"),
target = c("sp10", "sp12", "sp12", "sp22"),
w = c(2, 3, 2, 1))
trophic_from_df <- trophic(df_raw, from = "src", to = "target", weight = "w")
attr(trophic_from_df, "level")
#> sol sp10 sp12 sp22
#> 1 2 3 4trophic_df <- trophic() |>
add_link("sol", "sp10", 2) |>
add_link("sol", "sp12", 3) |>
add_link("sp10", "sp12", 2) |>
add_link("sp12", "sp22")
attr(trophic_df, "level")
#> sol sp10 sp12 sp22
#> 1 2 3 4The trophic web can then be computed:
A spacemodel is a list of two object a raster_stack and
a trophic_graph: - the raster_stack is a stack of rasters
using the terra package and the object
raster_stack - the trophic_graph is a directed
acyclic graph (DAG) to which we can add weight.
color_habitat <- colorRampPalette(c("white", "#169E19"))(255)
terra::plot(spcmdl_habitat, col=color_habitat)Dispersal is independent of trophic link for now since we did not yet implemented a dispersal dependent of the potential trophic links.
k_sp10 <- compute_kernel(radius=10, GSD=25, size_std=1.5)
k_sp12 <- compute_kernel(radius=100, GSD=25, size_std=1.5)
k_sp22 <- compute_kernel(radius=200, GSD=25, size_std=1.5)spcmdl_dispersal <- spcmdl_habitat |>
dispersal("sp10", method="convolution", method_option=list(kernel=k_sp10)) |>
dispersal("sp12", method="convolution", method_option=list(kernel=k_sp12)) |>
dispersal("sp22", method="convolution", method_option=list(kernel=k_sp22))Plot the dispersal object:
color_dispersal <- colorRampPalette(c("white", "#166C9E"))(255)
terra::plot(spcmdl_dispersal, col=color_dispersal)attr(spcmdl_dispersal, "trophic_tbl")
#> link weight
#> sp10 sol, sp10 2
#> sp12 sol, sp12 3
#> sp121 sp10, sp12 2
#> sp22 sp12, sp22 1test_intakes <- intake(spcmdl_dispersal,
# General rule: sp12 uptake with factor 3
"sp12" = 3,
# Exception: when sp12 feed on sp10, factor is 5
"sp10 -> sp12" = 5,
# General rule: sp22 has a complex function
"sp22" = ~ x^2 + 10,
# Exception: sol to sp10 is a coef
"sol -> sp10" = 0.1,
default = 1 # for all other default is 1
)
spcmdl_transfer <- transfer(spcmdl_dispersal, kernels, test_intakes)color_transfer <- colorRampPalette(c("white", "#A33D0A"))(255)
terra::plot(spcmdl_transfer, col=color_transfer)spacemodel for Eco-SSLThe first step is to build a raster stack with the ground as raster.
ground_cd <- load_raster_extdata("ground_concentration_cd_compressed.tif")
names_hab = c("soil", "plant", "invert", "mamHerb", "mamInsect", "birdInsect")
list_habitat <- lapply(names_hab, function(i) ground_cd)
stack_habitat <- raster_stack(list_habitat, names_hab)
terra::plot(stack_habitat)The second step is to build the trophic web, all species connecting directly to the soil.
trophic_df <- trophic() |>
add_link("soil", "plant") |>
add_link("soil", "invert") |>
add_link("soil", "mamHerb") |>
add_link("soil", "mamInsect") |>
add_link("soil", "birdInsect")
attr(trophic_df, "level")
#> soil plant invert mamHerb mamInsect birdInsect
#> 1 2 2 2 2 2The third step is to merge the raster stack with the trophic data.frame.
# no dispersal for eco_ssl
ecossl_kernels <- list(
soil = NA, plant = NA, invert = NA,
mamHerb = NA, mamInsect = NA, birdInsect = NA)ecossl_intakes <- intake(spcmdl_ecossl_h,
"soil -> plant" = ~ 10^x/32,
"soil -> invert" = ~ 10^x/140,
"soil -> mamHerb" = ~ 10^x/73,
"soil -> mamInsect" = ~ 10^x/0.36,
"soil -> birdInsect" = ~ 10^x/0.77,
default = 1, # for all other default is 1
normalize = FALSE # TRUE would weight every link to sum at 1
)
spcmdl_ecossl_risk <- transfer(
spcmdl_ecossl_h,
ecossl_kernels,
ecossl_intakes,
exposure_weighting="potential")A plot of layer with risk threshold color scale.
# Risk colors
breaks_risk <- c(0, 0.1, 0.5, 1, 5, 10, Inf)
cols_risk <- c(
"darkgreen", # 0 - 0.1
"green", # 0.1 - 0.5
"lightgreen", # 0.5 - 1
"yellow", # 1 - 5
"saddlebrown", # 5 - 10
"#4A2C2A" # > 10
)
names_keep <- names(spcmdl_ecossl_risk)[names(spcmdl_ecossl_risk) != "soil"]
spcmdl_ecossl_risk_sub <- spcmdl_ecossl_risk[[names_keep]]
terra::plot(spcmdl_ecossl_risk_sub,
breaks = breaks_risk,
col = cols_risk)poly <- roi_metaleurop
poly_vect <- terra::project(terra::vect(poly), terra::crs(spcmdl_ecossl_risk_sub))
rast_crop <- terra::crop(spcmdl_ecossl_risk_sub, poly_vect)
rast_final <- terra::mask(rast_crop, poly_vect)
terra::plot(rast_final,
breaks = breaks_risk,
col = cols_risk)For this very simple example, a simple check can be done, because Eco-SSL is a computing of a risk based on the amount in soil:
check_ecossl_risk = list(
"soil -> plant" = 10^ground_cd/32,
"soil -> invert" = 10^ground_cd/140,
"soil -> mamHerb" = 10^ground_cd/73,
"soil -> mamInsect" = 10^ground_cd/0.36,
"soil -> birdInsect" = 10^ground_cd/0.77
)
r_check_ecossl_risk = terra::rast(check_ecossl_risk)
terra::plot(r_check_ecossl_risk,
breaks = breaks_risk,
col = cols_risk)