"""The ``models.litter.chemistry`` module tracks the chemistry of the litter pools. This
involves both the polymer content (i.e. lignin content of the litter), as well as the
litter stoichiometry (i.e. nitrogen and phosphorus content).
The amount of lignin in both the structural pools and the dead wood pool is tracked, but
not for the metabolic pool because by definition it contains no lignin. Nitrogen and
phosphorus content are tracked for every pool.
Nitrogen and phosphorus contents do not have an explicit impact on decay rates, instead
these contents determine how input material is split between pools (see
:mod:`~virtual_ecosystem.models.litter.inputs`), which indirectly captures the
impact of N and P stoichiometry on litter decomposition rates. By contrast, the impact
of lignin on decay rates is directly calculated.
""" # noqa: D205
import numpy as np
from numpy.typing import NDArray
from xarray import DataArray
from virtual_ecosystem.core.data import Data
from virtual_ecosystem.models.litter.inputs import InputChemistries, LitterInputs
from virtual_ecosystem.models.litter.losses import LitterLosses
[docs]
class LitterChemistry:
"""This class handles the chemistry of litter pools.
This class contains methods to calculate the changes in the litter pool chemistry
based on the contents of the `data` object, as well as method to calculate total
mineralisation based on litter pool decay rates.
"""
def __init__(self, data: Data):
self.data = data
[docs]
def calculate_new_pool_chemistries(
self,
litter_inputs: LitterInputs,
litter_losses: LitterLosses,
input_chemistries: InputChemistries,
original_pools: dict[str, DataArray],
update_interval: float,
) -> dict[str, NDArray[np.floating]]:
"""Method to calculate the updated chemistry of each litter pool.
All pools contain nitrogen and phosphorus, so this is updated for every pool.
Only the structural (above and below ground) pools and the woody pools contain
lignin, so it is only updated for those pools.
Args:
litter_inputs: An LitterInputs instance containing the total input of each
plant biomass type, the proportion of the input that goes to the
relevant metabolic pool for each input type (expect deadwood) and the
total input into each litter pool.
litter_losses: An LitterLosses instance containing the total loss of carbon,
nitrogen and carbon from each litter pool.
input_chemistries: An InputChemistries instance containing the chemical
compositions of the input to each litter pool
original_pools: The size of each litter pool after animal consumption, but
before litter inputs and decay [kg{C} m^-2].
update_interval: The update interval for the litter model [days]
"""
# Then use to find the changes
new_lignin_proportions = self.calculate_new_lignin_proportions(
litter_inputs=litter_inputs,
input_chemistries=input_chemistries,
litter_losses=litter_losses,
original_pools=original_pools,
update_interval=update_interval,
)
new_nutrient_pools = calculate_updated_nutrient_pools(
input_chemistries=input_chemistries,
litter_losses=litter_losses,
original_pools=original_pools,
update_interval=update_interval,
)
return new_lignin_proportions | new_nutrient_pools
[docs]
def calculate_new_lignin_proportions(
self,
litter_inputs: LitterInputs,
input_chemistries: InputChemistries,
litter_losses: LitterLosses,
original_pools: dict[str, DataArray],
update_interval: float,
) -> dict[str, NDArray[np.floating]]:
"""Calculate the new lignin proportions for the relevant litter pools.
The relevant pools are the two structural pools, and the dead wood pool. This
function calculates the total change over the entire time step, so cannot be
used in an integration process.
Args:
litter_inputs: An LitterInputs instance containing the total input of each
plant biomass type, the proportion of the input that goes to the
relevant metabolic pool for each input type (expect deadwood) and the
total input into each litter pool.
input_chemistries: The chemical compositions of the input to each litter
pool
litter_losses: An LitterInputs instance containing the total loss of carbon,
nitrogen, phosphorus and lignin from each litter pool.
original_pools: The size of each litter pool after animal consumption, but
before litter inputs and decay [kg{C} m^-2].
update_interval: The update interval for the litter model [days]
Returns:
Dictionary containing the updated lignin proportions for the 3 relevant
litter pools (above ground structural, dead wood, and below ground
structural) [kg{lignin C} kg{C}^-1]
"""
new_lignin_proportion_above_struct = calculate_updated_pool_lignin_proportion(
initial_carbon=original_pools["above_structural"]
.sel(element="C")
.to_numpy(),
input_carbon_rate=litter_inputs.above_structural.to_numpy(),
carbon_loss=litter_losses.above_structural_carbon,
initial_lignin_proportion=self.data["lignin_above_structural"].to_numpy(),
input_lignin_proportion=input_chemistries.above_structural_lignin.to_numpy(),
lignin_loss=litter_losses.above_structural_lignin,
update_interval=update_interval,
)
new_lignin_proportion_woody = calculate_updated_pool_lignin_proportion(
initial_carbon=original_pools["woody"].sel(element="C").to_numpy(),
input_carbon_rate=litter_inputs.woody.to_numpy(),
carbon_loss=litter_losses.woody_carbon,
initial_lignin_proportion=self.data["lignin_woody"].to_numpy(),
input_lignin_proportion=input_chemistries.woody_lignin.to_numpy(),
lignin_loss=litter_losses.woody_lignin,
update_interval=update_interval,
)
new_lignin_proportion_below_struct = calculate_updated_pool_lignin_proportion(
initial_carbon=original_pools["below_structural"]
.sel(element="C")
.to_numpy(),
input_carbon_rate=litter_inputs.below_structural.to_numpy(),
carbon_loss=litter_losses.below_structural_carbon,
initial_lignin_proportion=self.data["lignin_below_structural"].to_numpy(),
input_lignin_proportion=input_chemistries.below_structural_lignin.to_numpy(),
lignin_loss=litter_losses.below_structural_lignin,
update_interval=update_interval,
)
return {
"lignin_above_structural": new_lignin_proportion_above_struct,
"lignin_woody": new_lignin_proportion_woody,
"lignin_below_structural": new_lignin_proportion_below_struct,
}
[docs]
def calculate_updated_nutrient_pools(
input_chemistries: InputChemistries,
litter_losses: LitterLosses,
original_pools: dict[str, DataArray],
update_interval: float,
) -> dict[str, NDArray[np.floating]]:
"""Calculate the updated nutrient masses for all litter pools.
This function calculates the total change over the entire time step, so cannot
be used in an integration process.
Args:
input_chemistries: The chemical compositions of the input to each litter
pool
litter_losses: An LitterInputs instance containing the total loss of carbon,
nitrogen and phosphorus from each litter pool.
original_pools: The size of each litter pool after animal consumption, but
before litter inputs and decay [kg{C} m^-2]
update_interval: The update interval for the litter model [days]
Returns:
Dictionary containing the updated carbon nitrogen ratios for all of the
litter pools [unitless]
"""
# Want to loop over all litter pools and elements in one go
elements = {
"N": "nitrogen",
"P": "phosphorus",
}
pools = [
"above_metabolic",
"above_structural",
"woody",
"below_metabolic",
"below_structural",
]
return {
f"{pool}_{full_name}": original_pools[f"{pool}"].sel(element=abbrev).to_numpy()
+ getattr(input_chemistries, f"{pool}_{full_name}") * update_interval
- getattr(litter_losses, f"{pool}_{full_name}")
for abbrev, full_name in elements.items()
for pool in pools
}
[docs]
def calculate_litter_chemistry_factor(
lignin_proportion: NDArray[np.floating], lignin_inhibition_factor: float
) -> NDArray[np.floating]:
"""Calculate the effect that litter chemistry has on litter decomposition rates.
This expression is taken from :cite:t:`kirschbaum_modelling_2002`.
Args:
lignin_proportion: The proportion of litter pool carbon that is held in the form
of lignin (or similar polymers) [kg{lignin C} kg{C}^-1]
lignin_inhibition_factor: An exponential factor expressing the extent to which
lignin inhibits the breakdown of litter [unitless]
Returns:
A factor that captures the impact of litter chemistry on litter decay rates
"""
return np.exp(lignin_inhibition_factor * lignin_proportion)
[docs]
def calculate_updated_pool_lignin_proportion(
initial_carbon: NDArray[np.floating],
input_carbon_rate: NDArray[np.floating],
carbon_loss: NDArray[np.floating],
initial_lignin_proportion: NDArray[np.floating],
input_lignin_proportion: NDArray[np.floating],
lignin_loss: NDArray[np.floating],
update_interval: float,
) -> NDArray[np.floating]:
"""Calculate the change in the lignin proportion of a particular litter pool.
The lignin proportion of the pool after the update is found by calculating the total
amounts of carbon (in any form) and lignin carbon, and then dividing to find the
proportion.
Args:
initial_carbon: The total carbon mass of the litter pool before inputs and decay
[kg{C} m^-2]
input_carbon_rate: The rate of carbon input to the litter pool
[kg{C} m^-2 day^-1]
carbon_loss: Total loss of carbon from the litter pool due to decay [kg{C} m^-2]
initial_lignin_proportion: The lignin proportion of the litter pool at the
start of the update interval [kg{lignin C} kg{C}^-1]
input_lignin_proportion: The lignin proportion of the input biomass
[kg{lignin C} kg{C}^-1]
lignin_loss: Total loss of lignin from the litter pool due to decay
[kg{lignin C} m^-2]
update_interval: The update interval for the litter model [days]
Returns:
The new lignin proportion at the end of the update interval
[kg{lignin C} kg{C}^-1]
"""
input_carbon_total = input_carbon_rate * update_interval
initial_lignin = initial_carbon * initial_lignin_proportion
input_lignin = input_carbon_total * input_lignin_proportion
return (initial_lignin + input_lignin - lignin_loss) / (
initial_carbon + input_carbon_total - carbon_loss
)