3 Plot locations and Tree Allometrics
A total of 46 forest plot sites were selected across the spatial extent of the project area. Plots locations were selected in order to capture a representative sample of the full biomass distribution across the site. This site selection was carried out through the inspection of LiDAR data collected in 2016 and satellite imagery. All plots were sampled twice, first in 2015/16 and then in 2021/2022. Figure 3.1 displays the locations of these points.
This target calculates the Total Above Ground Carbon (AGC) for every measured tree in each plot. The AGC is estimated using taxa-specific allometric models obtained from the {BIOMASS} R package (Réjou-Méchain et al. 2023). An interactive table of the full plot inventory is provided in Table 3.1.
3.1 Code
Tree Allometrics Code R/Kuamut_TreeAllometrics.R
save_tree_allometrics <- function(kuamut_AGB, site, save_csv) {
# Remove remaining columns no longer needed
# Save .csv file for next stage of analysis (see KuamutTree2Plot.R)
parent_dir <- set_path("PlotData")
if (!dir.exists(parent_dir)) dir.create(parent_dir)
tlm_out_path <- file.path(parent_dir, sprintf("%sTreeLevelMetrics.csv", site))
fin_agb_df <- kuamut_AGB |>
dplyr::select(!any_of(c(
"Distance_m", "Azimuth", "Notes", "Comments",
"butress", "levelWD", "PlotWD"
))) |>
dplyr::mutate(site = site)
if (isTRUE(save_csv)) {
fin_agb_df |>
write.csv(tlm_out_path, row.names = FALSE)
return(tlm_out_path)
} else {
return(tibble::tibble(fin_agb_df))
}
}
#' Tree Allometrics for Kuamut
#'
#' Generate Tree-level Above-Ground Biomass, Carbon and Volume
#'
#' @param ... csv file paths containing the raw plot data.
#' @param site default is "kuamut". Site name.
#' @param CF default is 0.47. Carbon Fraction
#' @param save_csv default is TRUE. Save the csv file.
#' @return csv file path.
#'
#' @details
#' This code takes the individual tree measurement and calculates Tree-level
#' Above-Ground Biomass, Carbon and Volume
#' Returns .csv file with Tree level AGB estimates
#' Written in accordance with VM0010, Version 1.2
#'
#' Tapering:
#'
#' When trees have large buttress roots, or other features making DBH
#' measurement at 1.3 meters is impossible. The Point Of Measurement (POM) is
#' raised from 1.3m and the POM recorded. We follow a standard protocol of
#' estimating the DBH at 1.3m using a tapering model
#'
#' We followed methods and equations in Cushman (2014) (but see also, Metcalf
#' et el. (2008), Jucker (2018), & Philipson (2020). We used the overall best
#' taper model (see also Metcalf et el. (2008)). Define function to calculate
#' equivalent diameter at 1.3 m given a diameter measurement in cm (DBH_cm)
#' taken at a nonstandard height in m (POM), and a taper parameter. We use the
#' geometric mean tapering parameter from Cushman (2014), which is 0.029 as per
#' Jucker (2018)
Kuamut_TreeAllometrics <- function(
...,
site = "kuamut", CF = 0.47, save_csv = TRUE) {
plts_csvs <- list(...)
# ---------------- 1. Load and clean Tree measurements data -----------------
read_trees <- function(plot_data) {
if (inherits(plot_data, "character")) {
plot_data <- read.csv(plot_data, header = TRUE)
}
plot_data |>
# remove two trees with missing DBH's keeps things neater
dplyr::filter(!is.na(DBH_mm)) |>
# Give a unique tree ID
mutate(
TreeID = paste(PlotNo, TreeNo, sep = "_"),
# Calculate DBH in cm (DBH was measured in mm for accuracy)
DBH_cm = DBH_mm / 10,
# fix excel dates
Date = lubridate::as_date(Date, origin = "1899-12-30")
) |>
# drop the full.names and DBH_mm columns
select(!any_of(c("DBH_mm", "Full.names", "Botanist"))) # names to drop
}
kuamut <- plts_csvs |>
purrr::map(~ read_trees(.x)) |>
bind_rows() |>
mutate(
MONTH = lubridate::round_date(Date, "month"),
YEAR = lubridate::year(Date),
CAMP_YEAR = case_when(
YEAR %in% c(2015:2016) ~ 2016,
YEAR == 2017 ~ YEAR,
YEAR == 2020 ~ YEAR,
YEAR %in% c(2021:2022) ~ 2022,
TRUE ~ NA_real_
)
)
# -------------------- 2. Tapering -------------------------------------------
# Define the Tapering Equation
TaperEqn <- function(DBH_cm, POM) {
# round DBH to 1 decimal place
round(DBH_cm / (exp(-0.029 * (POM - 1.3))), 1)
}
# If there is a POM value (measured above 1.3m), then apply tapering equation
# to DBH measurement. Otherwise use direct DBH measurement
kuamut_tap <- kuamut |>
mutate(DBH = case_when(
!is.na(POM) ~ TaperEqn(DBH_cm, POM),
TRUE ~ DBH_cm
)) |>
# remove DBH before tapering_cm and POM (no longer needed0)
select(!tidyselect::any_of(c("DBH_cm", "POM", "SolsOldName")))
# ----------- 3. Excluding some carbon pools (5.3, page 10 in VM0010) ------
# Excluding Aboveground non-tree measurement
# Liana biomass not included
# see Page 10 VM0010-v1.2 'Aboveground non-tree, Excluded'
# 'Exclusion is always conservative when forests remains as forest'
# Removes standing dead trees
# see Page 10 VM0010-v1.2 ''Dead wood, Excluded'
# take out all cases of standing dead trees D1,D2,D3, D4
kuamut_excl_cp <- kuamut_tap |>
dplyr::filter(
is.na(Notes) | Notes != "AL", # removes 35 liana measurements
!Notes %in% c("D1", "D2", "D3", "D4")
) # Removes standing dead trees
# -------------------- 4. Check Taxonomic Information -----------------------
# Corrects Synonyms spelling errors (uses BIOMASS package)
# Uses Taxonomic Name Resolution Service
# (http://tnrs.iplantcollaborative.org/; requires access to internet)
# Set up cache...
if (!dir.exists(here::here("BIOMASS_cache"))) {
dir.create(here::here("BIOMASS_cache"))
}
BIOMASS::createCache(path = here::here("BIOMASS_cache"))
# Correct taxa spelling and get family.
Taxo <- BIOMASS::correctTaxo(
genus = kuamut_excl_cp$Genus, species = kuamut_excl_cp$Species,
score = 0.5, useCache = TRUE
)
# Retrieve APG III Families and Orders from Genus names
Taxo <- mutate(Taxo, Family = BIOMASS::getTaxonomy(Taxo$genusCorr)$family)
# replaces our names with those corrected from the taxonic
# database (or if NA, use the name we had)
kuamut_cln_taxa <- kuamut_excl_cp |>
mutate(
Genus = case_when(
!is.na(Taxo$genusCorrected) ~ Taxo$genusCorrected,
TRUE ~ Genus
),
Species = case_when(
!is.na(Taxo$speciesCorrected) ~ Taxo$speciesCorrected,
TRUE ~ Species
),
Family = case_when(
!is.na(Taxo$Family) ~ Taxo$Family,
TRUE ~ Family
)
)
# ---------- 5. Assign Wood Density Data -----------------------------------
# Retrieve wood density from Global wood density database
dataWD <- BIOMASS::getWoodDensity(
genus = kuamut_cln_taxa$Genus,
species = kuamut_cln_taxa$Species,
family = kuamut_cln_taxa$Family
)
# Assign wood density (& indicator of the level of wood density (from Species,
# Genus or Family mean))
kuamut_WD <- kuamut_cln_taxa |>
mutate(
WD = round(dataWD$meanWD, 3),
levelWD = dataWD$levelWD,
WD = case_when(
levelWD == "dataset" ~ NA_real_,
TRUE ~ WD
) # replace 'dataset' values with NA's
)
# Plot level Basal Area-weighted mean wood density
# When we have no taxonomic information getWoodDensity() takes the average
# wood density of the dataset. It makes more sense to take the Basal Area
# Weighted mean for that specific plot
# Calculate Plot level Basal Area-weighted mean wood density
Plot_WD <- kuamut_WD |>
group_by(PlotNo) |>
dplyr::summarise(PlotWD = round(weighted.mean(WD, (pi * ((DBH) / 2)^2), na.rm = T), 3))
# Merge Plot level wood density into full dataset
kuamut_PWD <- kuamut_WD |>
left_join(Plot_WD, by = "PlotNo") |>
# For the trees with no taxonomic information, write 'PlotWD_w.mean' in the
# column 'levelWD'. Assign with the Basal Area weighted mean wood density
# for that plot.
# Assign trees with no taxonomic information with plot level Basal Area
# weighted mean wood density.
mutate(
levelWD = case_when(
is.na(WD) ~ "PlotWD",
TRUE ~ levelWD
),
WD = case_when(
is.na(WD) ~ PlotWD,
TRUE ~ WD
)
)
# ------------------- 6. Above-Ground Biomass estimation --------------------
# Calculate Tree-level Above-Ground Biomass (in Mg) according to Eq. 4 in
# Chave et al. (2014)
chave_2014_height_DBH <- function(.WD, .Height_m, .DBH) {
(0.0673 * (.WD * .Height_m * .DBH^2)^0.976) / 1000
}
# 3 param weibull height model, from our data plus others. previous version
# reported in philipson 2020
Hwei3p_sabah_cdp <- function(.DBH) {
(90.4997) * (1 - exp(-(0.0221) * .DBH^(0.7404)))
}
kuamut_AGB <- kuamut_PWD |>
dplyr::mutate(
height_src = case_when(
is.na(Height_m) ~ "modelled",
TRUE ~ "measured"
),
Height_m = dplyr::case_when(
is.na(Height_m) ~ Hwei3p_sabah_cdp(DBH),
TRUE ~ Height_m
),
AGB = chave_2014_height_DBH(WD, Height_m, DBH),
AGC = AGB * CF
) |>
dplyr::mutate(dplyr::across(c("AGB", "AGC"), \(x) round(x, 4)))
return(save_tree_allometrics(kuamut_AGB, site, save_csv))
}