Skip to contents

Aim

This vignette has been created to illustrate the creation of spatial inputs to be used in model simulations with the package, starting from forest inventory plot data.

Before reading this vignette, users should be familiar with forest and soil structures in package medfate. Moreover, a brief introduction to spatial structures used in medfateland package is given in vignette Package overview and examples are given in vignettes Spatially-uncoupled simulations.

Let’s first load necessary libraries:

The first section, A, shows how to create the spatial inputs from forest inventory data in tree lists, which will be the general case. The second section, B, shows the differences that occur when the starting point is a data structure issued from package forestables. For that part we need package forestables in our environment, because we will take an example data table from there. The package can be installed from GitHub:

# remotes::install_github("emf-creaf/forestables")
library(forestables)
## Loading required package: data.table
## Loading required package: dtplyr

A. General procedure

The functions introduced in this procedure are meant to be executed sequentially to progressively add spatial information, as illustrated in the workflow below, but users are free to use them in the most convenient way.

Initialization workflow for forest inventory data

Target locations

Package medfate includes a small dataset of tree data, corresponding to a dense holm oak forest in Poblet (Catalonia, Spain). As a result of the abandonment of previous coppicing exploitation, there is a high density of stems per individual. The data set includes data from a control plot and managed plot, where for the second two inventories have been carried out, one before management (thinning operation) and another afterwards. Hence, there are three stand structures.

We begin by defining an sf object with the target locations and forest stand identifiers (column id):

# Define sf with three stands
cc <- rbind(c(1.0215, 41.3432),
            c(1.0219, 41.3443), 
            c(1.0219, 41.3443))
d <- data.frame(lon = cc[,1], lat = cc[,2], 
                id = c("POBL_CTL", "POBL_THI_BEF", "POBL_THI_AFT"))
x <- sf::st_as_sf(d, coords = c("lon", "lat"), crs = 4326)
x
## Simple feature collection with 3 features and 1 field
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.0215 ymin: 41.3432 xmax: 1.0219 ymax: 41.3443
## Geodetic CRS:  WGS 84
##             id               geometry
## 1     POBL_CTL POINT (1.0215 41.3432)
## 2 POBL_THI_BEF POINT (1.0219 41.3443)
## 3 POBL_THI_AFT POINT (1.0219 41.3443)

where POBL_CTL is the control forest plot, POBL_THI_BEF is the managed plot before thinning and POBL_THI_AFT is the managed plot after thinning.

Topography

Once an object sf has been defined with target locations, we need to determine topographic features (elevation, slope, aspect) and land cover corresponding to those locations. You should have access to a Digital Elevation Model (DEM) at a desired resolution. Here we will use a DEM raster for Catalonia at 30 m resolution, which we load using package terra:

dataset_path <- "~/OneDrive/EMF_datasets/"
dem <- terra::rast(paste0(dataset_path,"Topography/Products/Catalunya/MET30m_ETRS89_UTM31_ICGC.tif"))
dem
## class       : SpatRaster 
## dimensions  : 9282, 9391, 1  (nrow, ncol, nlyr)
## resolution  : 30, 30  (x, y)
## extent      : 258097.5, 539827.5, 4485488, 4763948  (xmin, xmax, ymin, ymax)
## coord. ref. : ETRS89 / UTM zone 31N (EPSG:25831) 
## source      : MET30m_ETRS89_UTM31_ICGC.tif 
## name        : met15v20as0f0118Bmr1r050 
## min value   :                   -7.120 
## max value   :                 3133.625

Having these inputs, we can use function add_topography() to add topographic features to our starting sf:

y_1 <- add_topography(x, dem = dem, progress = FALSE)
## |---------|---------|---------|---------|=========================================                                          |---------|---------|---------|---------|=========================================                                          
y_1
## Simple feature collection with 3 features and 4 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.0215 ymin: 41.3432 xmax: 1.0219 ymax: 41.3443
## Geodetic CRS:  WGS 84
## # A tibble: 3 × 5
##   id                   geometry elevation slope aspect
##   <chr>             <POINT [°]>     <dbl> <dbl>  <dbl>
## 1 POBL_CTL     (1.0215 41.3432)      853.  30.1   76.0
## 2 POBL_THI_BEF (1.0219 41.3443)      814.  29.3   40.3
## 3 POBL_THI_AFT (1.0219 41.3443)      814.  29.3   40.3

We can check that there are no missing values in topographic features:

##  No missing values in topography.

Forest inventory data parsing

The next step is to define forest objects for our simulations. Forests should be defined for all target locations whose land cover is defined as wildland (here, all three stands). In our case, we will define forest objects from tree records in Poblet. While at this point you would read your own data from a file or data base, here we simply load the data from medfate:

data(poblet_trees)
head(poblet_trees)
##   Plot.Code Indv.Ref             Species Diameter.cm
## 1  POBL_CTL        1 Acer monspessulanum         7.6
## 2  POBL_CTL        2       Arbutus unedo         7.5
## 3  POBL_CTL        3       Arbutus unedo         7.5
## 4  POBL_CTL        4       Arbutus unedo         7.5
## 5  POBL_CTL        5       Arbutus unedo         7.5
## 6  POBL_CTL        6       Arbutus unedo         7.5

There are only four columns in the data set. Plot.Code identifies the forest stand, Indv.Ref is the tree number, Species is the scientific name of the tree species and Diameter.cm is the tree diameter in cm. To properly parse forest inventory data, we need to define a mapping from these variables into names meaningful for medfate. For this, we can check function forest_mapTreeTable() in the package. Furthermore, we need to map the forest stand identifier. If we exclude Indv.Ref, the other variables would be mapped using:

mapping <- c("id" = "Plot.Code", "Species.name" = "Species", "DBH" = "Diameter.cm")

Now we are ready to call function add_forests(), which should facilitate the creation of forest objects:

y_2 <- add_forests(y_1, tree_table = poblet_trees, tree_mapping = mapping, 
                   SpParams = SpParamsMED)
## Warning in forest_mapTreeTable(x = tree_id, mapping_x = tree_mapping, SpParams
## = SpParams): Taxon names that were not matched: Quercus humilis.
## Warning in forest_mapTreeTable(x = tree_id, mapping_x = tree_mapping, SpParams
## = SpParams): Taxon names that were not matched: Quercus humilis.

Note that two warnings were raised, informing us that Quercus humilis (downy oak) was not matched to any species name in SpParamsMED (that is the reason why we provided it as an input). We correct the scientific name for downy oak and repeat to avoid losing tree records:

poblet_trees$Species[poblet_trees$Species=="Quercus humilis"] <- "Quercus pubescens"
y_2 <- add_forests(y_1, tree_table = poblet_trees, tree_mapping = mapping, 
                   SpParams = SpParamsMED)
y_2
## Simple feature collection with 3 features and 5 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.0215 ymin: 41.3432 xmax: 1.0219 ymax: 41.3443
## Geodetic CRS:  WGS 84
## # A tibble: 3 × 6
##   id                   geometry elevation slope aspect forest      
##   <chr>             <POINT [°]>     <dbl> <dbl>  <dbl> <list>      
## 1 POBL_CTL     (1.0215 41.3432)      853.  30.1   76.0 <forest [5]>
## 2 POBL_THI_BEF (1.0219 41.3443)      814.  29.3   40.3 <forest [5]>
## 3 POBL_THI_AFT (1.0219 41.3443)      814.  29.3   40.3 <forest [5]>

The function has added a column forest with the tree data. We can check the metrics of the first forest stand using:

summary(y_2$forest[[1]], SpParamsMED)
## Tree BA (m2/ha): 3.0179815  adult trees: 3.0179815  saplings: 0 
## Density (ind/ha) adult trees: 267  saplings: 0  shrubs (estimated): 0 
## Cover (%) adult trees: 42.3272186  saplings: 0  shrubs: 0  herbs: 0 
## LAI (m2/m2) total: 0.5419688  adult trees: 0.5419688  saplings: 0  shrubs: 0  herbs: 0 
## Fuel loading (kg/m2) total: 0.1414227  adult trees: 0.1414227  saplings: 0  shrubs: 0  herbs: 0 
## PAR ground (%): NA  SWR ground (%): NA

Both density and tree basal area seem unusually low. This is because we did not specify the sampled area, and tree data in forest objects require a density estimation. By default, each record (row in the input data frame) will count as a tree, but we can add the sampled plot surface to correct density per hectare. In a 15-m radius plot, the sampled surface is 706.86 m2. We add this information in the data table:

poblet_trees$PlotSurface <- 706.86

and we can repeat the read of forest inventory data after adding a new element to the mapping vector:

mapping <- c(mapping, "plot.size" = "PlotSurface")
y_2 <- add_forests(y_1, tree_table = poblet_trees, tree_mapping = mapping, SpParams = SpParamsMED)

If we check the forest again, we will see the change in density and basal area:

summary(y_2$forest[[1]], SpParamsMED)
## Tree BA (m2/ha): 42.6956049  adult trees: 42.6956049  saplings: 0 
## Density (ind/ha) adult trees: 3777.2684832  saplings: 0  shrubs (estimated): 0 
## Cover (%) adult trees: 100  saplings: 0  shrubs: 0  herbs: 0 
## LAI (m2/m2) total: 4.9189289  adult trees: 4.9189289  saplings: 0  shrubs: 0  herbs: 0 
## Fuel loading (kg/m2) total: 1.2992685  adult trees: 1.2992685  saplings: 0  shrubs: 0  herbs: 0 
## PAR ground (%): NA  SWR ground (%): NA

At this point, we may wonder whether we are finished. Function check_forests() can help detecting missing data:

##  No wildland locations with NULL values in column 'forest'.
##  All objects in column 'forest' have the right class.
## ! Missing tree height values detected for 28 (100%) in 3 wildland locations (100%).

The function alerts us that height values are missing. Since they are required for simulations using medfate, we need to estimate them. While being overly simplistic, here we use one allometric equation for all species to estimate tree height in cm from diameter:

poblet_trees$Height.cm <- 100 * 1.806*poblet_trees$Diameter.cm^0.518

Now we modify the mapping to include height and repeat the reading operation:

mapping <- c(mapping, "Height" = "Height.cm")
y_2 <- add_forests(y_1, tree_table = poblet_trees, tree_mapping = mapping, SpParams = SpParamsMED)

If we check the forest data again, we should encounter no more issues:

##  No wildland locations with NULL values in column 'forest'.
##  All objects in column 'forest' have the right class.
##  No missing/wrong values detected in key tree/shrub attributes of 'forest' objects.

Soil parameterization

Soil information is most usually lacking for the target locations. Regional maps of soil properties may be available in some cases. Here we assume this information is not available, so that we resort to global products. In particular, we will use information provided in SoilGrids at 250 m resolution (Hengl et al. (2017); Poggio et al. (2021)).

SoilGrids 2.0 data

Function add_soilgrids() can perform queries using the REST API of SoilGrids, but this becomes problematic for multiple sites. Hence, we recommend downloading SoilGrid rasters for the target region and storing them in a particular format, so that function add_soilgrids() can read them (check the details of the function documentation). The extraction of SoilGrids data for our target locations is fast using this approach:

soilgrids_path = paste0(dataset_path,"Soils/Sources/Global/SoilGrids/Spain/")
y_3 <- add_soilgrids(y_2, soilgrids_path = soilgrids_path, progress = FALSE)

And the result has an extra column soil:

y_3
## Simple feature collection with 3 features and 6 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.0215 ymin: 41.3432 xmax: 1.0219 ymax: 41.3443
## Geodetic CRS:  WGS 84
## # A tibble: 3 × 7
##   id                   geometry elevation slope aspect forest       soil        
##   <chr>             <POINT [°]>     <dbl> <dbl>  <dbl> <list>       <list>      
## 1 POBL_CTL     (1.0215 41.3432)      853.  30.1   76.0 <forest [5]> <df [6 × 7]>
## 2 POBL_THI_BEF (1.0219 41.3443)      814.  29.3   40.3 <forest [5]> <df [6 × 7]>
## 3 POBL_THI_AFT (1.0219 41.3443)      814.  29.3   40.3 <forest [5]> <df [6 × 7]>

The elements of the list are the usual data frames of soil properties in medfate:

y_3$soil[[1]]
##   widths clay sand   om   bd  rfc nitrogen
## 1     50 25.3 32.1 7.24 1.05 16.8     5.07
## 2    100 25.5 32.0 3.10 1.16 18.7     2.18
## 3    150 29.9 31.2 1.96 1.25 19.8     1.35
## 4    300 30.7 30.7 1.00 1.39 19.6     0.81
## 5    400 30.4 30.2 0.89 1.49 20.4     0.58
## 6   1000 32.0 30.2 0.62 1.50 21.5     0.44

We can use function check_soils() to detect whether there are missing values:

##  No wildland/agriculture locations with NULL values in column 'soil'.
##  No missing values detected in key soil attributes.

Soil depth and rock content modification

SoilGrids 2.0 does not provide information on soil depth, and rock fragment content is normally underestimated, which leads to an overestimation of water holding capacity. Function modify_soils() allows modifying soil definitions, if information is available for soil depth, depth to the (unaltered) bedrock, or both. Soil depth maps are not common in many regions, so here we will resort on a global product at 250m-resolution by Shangguan et al. (2017), which consists on three rasters:

# Censored soil depth (cm)
bdricm <- terra::rast(paste0(dataset_path, "Soils/Sources/Global/SoilDepth_Shangguan2017/BDRICM_M_250m_ll.tif"))
# Probability of bedrock within first 2m [0-100]
bdrlog <- terra::rast(paste0(dataset_path, "Soils/Sources/Global/SoilDepth_Shangguan2017/BDRLOG_M_250m_ll.tif"))
# Absolute depth to bedrock (cm)
bdticm <- terra::rast(paste0(dataset_path, "Soils/Sources/Global/SoilDepth_Shangguan2017/BDTICM_M_250m_ll.tif"))

In order to accelerate raster manipulations, we crop the global rasters to the extent of the target area:

x_vect <- terra::vect(sf::st_transform(sf::st_geometry(x), terra::crs(bdricm)))
x_ext <- terra::ext(x_vect)
bdricm <- terra::crop(bdricm, x_ext, snap = "out")
bdrlog <- terra::crop(bdrlog, x_ext, snap = "out")
bdticm <- terra::crop(bdticm, x_ext, snap = "out")

Censored soil depth is a poor product of actual soil depth, but we have observed a fairly good correlation between soil depth values in Catalonia and the probability of finding the bedrock within the first two meters. Hence, we multiply the two layers and use it as a (crude) estimate of soil depth, expressing it in mm:

soil_depth_mm <- (bdricm$BDRICM_M_250m_ll*10)*(1 - (bdrlog$BDRLOG_M_250m_ll/100))

and we take the depth to bedrock as appropriate, but change its units to mm as well:

depth_to_bedrock_mm <- bdticm*10

We can now call function modify_soils() with the two rasters to perform the correction of soil characteristics:

y_4 <- modify_soils(y_3, 
                    soil_depth_map = soil_depth_mm, 
                    depth_to_bedrock_map = depth_to_bedrock_mm,
                    progress = FALSE)

In this case, the depth to bedrock values were deeper than 2m, so that only the soil depth map had an effect on the correction procedure. After the correction, the rock fragment content of the soil has changed substantially:

y_4$soil[[1]]
##   widths clay sand   om   bd      rfc nitrogen
## 1     50 25.3 32.1 7.24 1.05 16.80000     5.07
## 2    100 25.5 32.0 3.10 1.16 18.70000     2.18
## 3    150 29.9 31.2 1.96 1.25 19.80000     1.35
## 4    300 30.7 30.7 1.00 1.39 27.76899     0.81
## 5    400 30.4 30.2 0.89 1.49 49.36709     0.58
## 6   1000 32.0 30.2 0.62 1.50 92.56329     0.44

Finally, we can call again check_soils() to verify that everything is fine:

##  No wildland/agriculture locations with NULL values in column 'soil'.
##  No missing values detected in key soil attributes.

Initialization test

We can check whether the input data set is well formed by calling function initialize_landscape():

z <- initialize_landscape(y_4, SpParamsMED, defaultControl(),
                          progress = FALSE)
z
## Simple feature collection with 3 features and 7 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.0215 ymin: 41.3432 xmax: 1.0219 ymax: 41.3443
## Geodetic CRS:  WGS 84
## # A tibble: 3 × 8
##   id                   geometry elevation slope aspect forest       soil        
## * <chr>             <POINT [°]>     <dbl> <dbl>  <dbl> <list>       <list>      
## 1 POBL_CTL     (1.0215 41.3432)      853.  30.1   76.0 <forest [5]> <df [6 × 7]>
## 2 POBL_THI_BEF (1.0219 41.3443)      814.  29.3   40.3 <forest [5]> <df [6 × 7]>
## 3 POBL_THI_AFT (1.0219 41.3443)      814.  29.3   40.3 <forest [5]> <df [6 × 7]>
## # ℹ 1 more variable: state <list>

Everything seems fine for simulations!

B. Procedure from forestables

R package forestables allows reading and harmonizing national forest inventory data from the FIA (USA forest inventory), FFI (France forest inventory) and IFN (Spain forest inventory). When an data issued from this package is the starting point to create spatial inputs for medfateland, the general procedure is simplified, as shown below:

Example dataset

To illustrate this procedure, we first load an example data set given in the forestables package for the Spanish national forest inventory:

data("ifn_output_example")
ifn_output_example
## # A tibble: 8,997 × 24
##    id_unique_code    year plot  coordx coordy coord_sys   crs  elev aspect slope
##    <chr>            <int> <chr>  <dbl>  <dbl> <chr>     <dbl> <dbl>  <dbl> <dbl>
##  1 08_0001_NN_A1_A1  1990 0001  402000 4.68e6 ED50      23031  1900     NA    NA
##  2 08_0002_NN_A1_A1  1990 0002  400000 4.68e6 ED50      23031  1700     NA    NA
##  3 08_0003_NN_A1_A1  1990 0003  401000 4.68e6 ED50      23031  1700     NA    NA
##  4 08_0004_NN_A1_A1  1990 0004  402000 4.68e6 ED50      23031  1400     NA    NA
##  5 08_0005_NN_A1_A1  1990 0005  400000 4.68e6 ED50      23031  1300     NA    NA
##  6 08_0006_NN_A1_A1  1990 0006  397000 4.68e6 ED50      23031  1700     NA    NA
##  7 08_0007_NN_A1_xx  1990 0007  399000 4.68e6 ED50      23031  1400     NA    NA
##  8 08_0008_NN_A1_xx  1990 0008  401000 4.68e6 ED50      23031  1100     NA    NA
##  9 08_0009_NN_A1_xx  1990 0009  402000 4.68e6 ED50      23031  1100     52    NA
## 10 08_0010_NN_A1_xx  1990 0010  394000 4.68e6 ED50      23031  1500     NA    NA
## # ℹ 8,987 more rows
## # ℹ 14 more variables: country <chr>, version <chr>, class <chr>,
## #   subclass <chr>, province_code <chr>, province_name_original <chr>,
## #   ca_name_original <chr>, sheet_ntm <chr>, huso <dbl>, slope_mean <chr>,
## #   type <int>, tree <list>, understory <list>, regen <list>

The dataset contains data from different forest inventory surveys:

table(ifn_output_example$version)
## 
## ifn2 ifn3 ifn4 
## 3542 3769 1686

Here we will use only data from the last survey (IFN4):

ifn4_example <- ifn_output_example |>
  dplyr::filter(version == "ifn4")
ifn4_example
## # A tibble: 1,686 × 24
##    id_unique_code    year plot  coordx coordy coord_sys   crs  elev aspect slope
##    <chr>            <int> <chr>  <dbl>  <dbl> <chr>     <dbl> <dbl>  <dbl> <dbl>
##  1 08_0001_NN_A1_A1  2015 0001  401922 4.68e6 ED50      23031    NA   202.    40
##  2 08_0002_NN_A1_A1  2014 0002  399895 4.68e6 ED50      23031    NA   342     40
##  3 08_0003_NN_A1_A1  2015 0003  400898 4.68e6 ED50      23031    NA    99     40
##  4 08_0004_NN_A1_A1  2014 0004  401903 4.68e6 ED50      23031    NA   292.    40
##  5 08_0005_NN_A1_A1  2014 0005  399917 4.68e6 ED50      23031    NA    99     40
##  6 08_0006_NN_A1_A1  2014 0006  396931 4.68e6 ED50      23031    NA   351     40
##  7 08_0009_xx_xx_A4  2016 0009  401899 4.68e6 ED50      23031    NA    90     40
##  8 08_0014_NN_A1_A1  2015 0014  397927 4.68e6 ED50      23031    NA   234     40
##  9 08_0016_xx_xx_A4  2014 0016  392906 4.68e6 ED50      23031    NA   346.    40
## 10 08_0020_NN_A1_A1  2015 0020  397842 4.68e6 ED50      23031    NA    36     40
## # ℹ 1,676 more rows
## # ℹ 14 more variables: country <chr>, version <chr>, class <chr>,
## #   subclass <chr>, province_code <chr>, province_name_original <chr>,
## #   ca_name_original <chr>, sheet_ntm <chr>, huso <dbl>, slope_mean <chr>,
## #   type <int>, tree <list>, understory <list>, regen <list>

Parsing forestable data

Function parse_forestable() allows parsing a data frame (or sf) issued from package forestables and reshaping it for medfateland. In this example we will only transform the first 100 rows:

y_1 <- parse_forestable(ifn4_example[1:100,])
y_1
## Simple feature collection with 100 features and 10 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 1.689586 ymin: 42.14464 xmax: 2.041201 ymax: 42.29968
## Geodetic CRS:  WGS 84
## # A tibble: 100 × 11
##    id                        geometry  year plot  country version regen   
##    <chr>                  <POINT [°]> <int> <chr> <chr>   <chr>   <list>  
##  1 08_0001_NN_A1… (1.809047 42.29746)  2015 0001  ES      ifn4    <tibble>
##  2 08_0002_NN_A1… (1.784613 42.28934)  2014 0002  ES      ifn4    <tibble>
##  3 08_0003_NN_A1… (1.796776 42.28951)  2015 0003  ES      ifn4    <tibble>
##  4 08_0004_NN_A1… (1.808972 42.28922)  2014 0004  ES      ifn4    <tibble>
##  5 08_0005_NN_A1… (1.785068 42.27957)  2014 0005  ES      ifn4    <tibble>
##  6 08_0006_NN_A1… (1.749024 42.27098)  2014 0006  ES      ifn4    <tibble>
##  7 08_0009_xx_xx…  (1.809254 42.2717)  2016 0009  ES      ifn4    <tibble>
##  8 08_0014_NN_A1… (1.761286 42.26156)  2015 0014  ES      ifn4    <tibble>
##  9 08_0016_xx_xx… (1.700602 42.25217)  2014 0016  ES      ifn4    <tibble>
## 10 08_0020_NN_A1… (1.760414 42.25344)  2015 0020  ES      ifn4    <tibble>
## # ℹ 90 more rows
## # ℹ 4 more variables: elevation <dbl>, slope <dbl>, aspect <dbl>, forest <list>

As can be seen from the output, the function parsed plot ids, plot coordinates, and topography. Importantly, it defined a new column called forest and parsed tree and shrub data into it.

If we check the forest data, we may encounter issues to be solved:

##  No wildland locations with NULL values in column 'forest'.
##  All objects in column 'forest' have the right class.
##  No missing/wrong values detected in key tree/shrub attributes of 'forest' objects.

Remaining steps

The remaining steps are similar to the general procedure, but are repeated here for completeness.

Topography data

Topographic information in national forest inventory data can contain a lot of missing values. It is normally preferrable to extract topographic features from a digital elevation model (although the error in plot coordinates adds uncertainty into the result):

y_2 <- add_topography(y_1, dem = dem, progress = FALSE)
## |---------|---------|---------|---------|=========================================                                          |---------|---------|---------|---------|=========================================                                          

Soil data

Soil data is not normally sampled in national forest inventory data, or it is difficult to harmonize. Therefore, we will normally resort on SoilGrids as before:

y_3 <- add_soilgrids(y_2, soilgrids_path = soilgrids_path, progress = FALSE)

We can now call function modify_soils() with the two rasters to perform the correction of soil characteristics:

y_4 <- modify_soils(y_3, 
                    soil_depth_map = soil_depth_mm, 
                    depth_to_bedrock_map = depth_to_bedrock_mm,
                    progress = FALSE)

Finally, we can call check_soils() to verify that everything is fine:

##  No wildland/agriculture locations with NULL values in column 'soil'.
##  No missing values detected in key soil attributes.

Initialization test

At this point we should test the initialization. Here, however, we should use an SpParams data frame having all the species mentioned in the forest inventory data.