"""The ``models.litter.losses`` module handles the calculation of the total loss of each
nutrient (carbon, nitrogen and phosphorus) from each litter pool, as well as the total
mineralisation rate to soil of each nutrient.
""" # noqa: D205
from dataclasses import dataclass
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
[docs]
@dataclass(frozen=True)
class LitterLosses:
"""The full set losses for the litter pools, as well as the mineralisation rates."""
above_metabolic_carbon: NDArray[np.floating]
"""Carbon loss from the aboveground metabolic pool [kg{C} m^-2]"""
above_structural_carbon: NDArray[np.floating]
"""Carbon loss from the aboveground structural pool [kg{C} m^-2]"""
woody_carbon: NDArray[np.floating]
"""Carbon loss from the woody pool [kg{C} m^-2]"""
below_metabolic_carbon: NDArray[np.floating]
"""Carbon loss from the belowground metabolic pool [kg{C} m^-2]"""
below_structural_carbon: NDArray[np.floating]
"""Carbon loss from the belowground structural pool [kg{C} m^-2]"""
above_metabolic_nitrogen: NDArray[np.floating]
"""Nitrogen loss from the aboveground metabolic pool [kg{N} m^-2]"""
above_structural_nitrogen: NDArray[np.floating]
"""Nitrogen loss from the aboveground structural pool [kg{N} m^-2]"""
woody_nitrogen: NDArray[np.floating]
"""Nitrogen loss from the woody pool [kg{N} m^-2]"""
below_metabolic_nitrogen: NDArray[np.floating]
"""Nitrogen loss from the belowground metabolic pool [kg{N} m^-2]"""
below_structural_nitrogen: NDArray[np.floating]
"""Nitrogen loss from the belowground structural pool [kg{N} m^-2]"""
above_metabolic_phosphorus: NDArray[np.floating]
"""Phosphorus loss from the aboveground metabolic pool [kg{P} m^-2]"""
above_structural_phosphorus: NDArray[np.floating]
"""Phosphorus loss from the aboveground structural pool [kg{P} m^-2]"""
woody_phosphorus: NDArray[np.floating]
"""Phosphorus loss from the woody pool [kg{P} m^-2]"""
below_metabolic_phosphorus: NDArray[np.floating]
"""Phosphorus loss from the belowground metabolic pool [kg{P} m^-2]"""
below_structural_phosphorus: NDArray[np.floating]
"""Phosphorus loss from the belowground structural pool [kg{P} m^-2]"""
above_structural_lignin: NDArray[np.floating]
"""Lignin loss from the aboveground structural pool [kg{lignin C} m^-2]"""
woody_lignin: NDArray[np.floating]
"""Lignin loss from the woody pool [kg{lignin C} m^-2]"""
below_structural_lignin: NDArray[np.floating]
"""Lignin loss from the belowground structural pool [kg{lignin C} m^-2]"""
N_mineralisation_rate: NDArray[np.floating]
"""Total nitrogen mineralisation rate from all litter pools [kg{N} m^-3 day^-1]"""
P_mineralisation_rate: NDArray[np.floating]
"""Total phosphorus mineralisation rate from all litter pools [kg{P} m^-3 day^-1]"""
[docs]
def calculate_litter_losses(
data: Data,
original_pools: dict[str, DataArray],
final_pools: dict[str, NDArray[np.floating]],
litter_inputs: LitterInputs,
input_chemistries: InputChemistries,
update_interval: float,
active_microbe_depth: float,
) -> LitterLosses:
"""Calculate the loss of carbon, nitrogen and phosphorus from each litter pool.
Total mineralisation rates to soil for nitrogen and phosphorus are also calculated.
Args:
data: A :class:`~virtual_ecosystem.core.data.Data` instance.
original_pools: Pool sizes before any litter input and decay [kg{C} m^-2].
final_pools: Pool sizes after litter input and decay [kg{C} m^-2].
litter_inputs: The inputs to each litter pool [kg{C} m^-2 day^-1].
input_chemistries: The chemical compositions of the inputs to each litter pool.
update_interval: The time period over which the litter pools are updated [days].
active_microbe_depth: The depth at which microbial activity is assumed to cease
[m].
Returns:
A dataclass containing the total losses of each nutrient from each litter pool,
as well as the total mineralisation rates to the soil for each nutrient.
"""
# Calculate the loss of carbon from each litter pool
above_metabolic_carbon = calculate_carbon_pool_loss(
old_pool_size=original_pools["above_metabolic"].sel(element="C").to_numpy(),
final_pool_size=final_pools["above_metabolic"],
input_rate=litter_inputs.above_metabolic.to_numpy(),
update_interval=update_interval,
)
above_structural_carbon = calculate_carbon_pool_loss(
old_pool_size=original_pools["above_structural"].sel(element="C").to_numpy(),
final_pool_size=final_pools["above_structural"],
input_rate=litter_inputs.above_structural.to_numpy(),
update_interval=update_interval,
)
woody_carbon = calculate_carbon_pool_loss(
old_pool_size=original_pools["woody"].sel(element="C").to_numpy(),
final_pool_size=final_pools["woody"],
input_rate=litter_inputs.woody.to_numpy(),
update_interval=update_interval,
)
below_metabolic_carbon = calculate_carbon_pool_loss(
old_pool_size=original_pools["below_metabolic"].sel(element="C").to_numpy(),
final_pool_size=final_pools["below_metabolic"],
input_rate=litter_inputs.below_metabolic.to_numpy(),
update_interval=update_interval,
)
below_structural_carbon = calculate_carbon_pool_loss(
old_pool_size=original_pools["below_structural"].sel(element="C").to_numpy(),
final_pool_size=final_pools["below_structural"],
input_rate=litter_inputs.below_structural.to_numpy(),
update_interval=update_interval,
)
# Calculate the loss of nitrogen from each litter pool
above_metabolic_nitrogen = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["above_metabolic"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["above_metabolic"]
.sel(element="N")
.to_numpy(),
carbon_loss=above_metabolic_carbon,
input_rate_carbon=litter_inputs.above_metabolic.to_numpy(),
input_rate_nutrient=input_chemistries.above_metabolic_nitrogen.to_numpy(),
update_interval=update_interval,
)
above_structural_nitrogen = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["above_structural"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["above_structural"]
.sel(element="N")
.to_numpy(),
carbon_loss=above_structural_carbon,
input_rate_carbon=litter_inputs.above_structural.to_numpy(),
input_rate_nutrient=input_chemistries.above_structural_nitrogen.to_numpy(),
update_interval=update_interval,
)
woody_nitrogen = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["woody"].sel(element="C").to_numpy(),
initial_pool_nutrient=original_pools["woody"].sel(element="N").to_numpy(),
carbon_loss=woody_carbon,
input_rate_carbon=litter_inputs.woody.to_numpy(),
input_rate_nutrient=input_chemistries.woody_nitrogen.to_numpy(),
update_interval=update_interval,
)
below_metabolic_nitrogen = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["below_metabolic"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["below_metabolic"]
.sel(element="N")
.to_numpy(),
carbon_loss=below_metabolic_carbon,
input_rate_carbon=litter_inputs.below_metabolic.to_numpy(),
input_rate_nutrient=input_chemistries.below_metabolic_nitrogen.to_numpy(),
update_interval=update_interval,
)
below_structural_nitrogen = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["below_structural"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["below_structural"]
.sel(element="N")
.to_numpy(),
carbon_loss=below_structural_carbon,
input_rate_carbon=litter_inputs.below_structural.to_numpy(),
input_rate_nutrient=input_chemistries.below_structural_nitrogen.to_numpy(),
update_interval=update_interval,
)
# Calculate the loss of nitrogen from each litter pool
above_metabolic_phosphorus = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["above_metabolic"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["above_metabolic"]
.sel(element="P")
.to_numpy(),
carbon_loss=above_metabolic_carbon,
input_rate_carbon=litter_inputs.above_metabolic.to_numpy(),
input_rate_nutrient=input_chemistries.above_metabolic_phosphorus.to_numpy(),
update_interval=update_interval,
)
above_structural_phosphorus = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["above_structural"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["above_structural"]
.sel(element="P")
.to_numpy(),
carbon_loss=above_structural_carbon,
input_rate_carbon=litter_inputs.above_structural.to_numpy(),
input_rate_nutrient=input_chemistries.above_structural_phosphorus.to_numpy(),
update_interval=update_interval,
)
woody_phosphorus = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["woody"].sel(element="C").to_numpy(),
initial_pool_nutrient=original_pools["woody"].sel(element="P").to_numpy(),
carbon_loss=woody_carbon,
input_rate_carbon=litter_inputs.woody.to_numpy(),
input_rate_nutrient=input_chemistries.woody_phosphorus.to_numpy(),
update_interval=update_interval,
)
below_metabolic_phosphorus = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["below_metabolic"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["below_metabolic"]
.sel(element="P")
.to_numpy(),
carbon_loss=below_metabolic_carbon,
input_rate_carbon=litter_inputs.below_metabolic.to_numpy(),
input_rate_nutrient=input_chemistries.below_metabolic_phosphorus.to_numpy(),
update_interval=update_interval,
)
below_structural_phosphorus = calculate_nutrient_pool_loss(
initial_pool_carbon=original_pools["below_structural"]
.sel(element="C")
.to_numpy(),
initial_pool_nutrient=original_pools["below_structural"]
.sel(element="P")
.to_numpy(),
carbon_loss=below_structural_carbon,
input_rate_carbon=litter_inputs.below_structural.to_numpy(),
input_rate_nutrient=input_chemistries.below_structural_phosphorus.to_numpy(),
update_interval=update_interval,
)
# Calculate the loss of lignin from the three relevant litter pools
above_structural_lignin = calculate_lignin_pool_loss(
initial_pool_size=original_pools["above_structural"]
.sel(element="C")
.to_numpy(),
carbon_loss=above_structural_carbon,
input_rate=litter_inputs.above_structural.to_numpy(),
initial_lignin_proportion=data["lignin_above_structural"].to_numpy(),
input_lignin_proportion=input_chemistries.above_structural_lignin.to_numpy(),
update_interval=update_interval,
)
woody_lignin = calculate_lignin_pool_loss(
initial_pool_size=original_pools["woody"].sel(element="C").to_numpy(),
carbon_loss=woody_carbon,
input_rate=litter_inputs.woody.to_numpy(),
initial_lignin_proportion=data["lignin_woody"].to_numpy(),
input_lignin_proportion=input_chemistries.woody_lignin.to_numpy(),
update_interval=update_interval,
)
below_structural_lignin = calculate_lignin_pool_loss(
initial_pool_size=original_pools["below_structural"]
.sel(element="C")
.to_numpy(),
carbon_loss=below_structural_carbon,
input_rate=litter_inputs.below_structural.to_numpy(),
initial_lignin_proportion=data["lignin_below_structural"].to_numpy(),
input_lignin_proportion=input_chemistries.below_structural_lignin.to_numpy(),
update_interval=update_interval,
)
# Finally, calculate the total mineralisation rates for nitrogen and phosphorus
N_mineralisation_rate = (
above_metabolic_nitrogen
+ above_structural_nitrogen
+ woody_nitrogen
+ below_metabolic_nitrogen
+ below_structural_nitrogen
) / (update_interval * active_microbe_depth)
P_mineralisation_rate = (
above_metabolic_phosphorus
+ above_structural_phosphorus
+ woody_phosphorus
+ below_metabolic_phosphorus
+ below_structural_phosphorus
) / (update_interval * active_microbe_depth)
return LitterLosses(
above_metabolic_carbon=above_metabolic_carbon,
above_structural_carbon=above_structural_carbon,
woody_carbon=woody_carbon,
below_metabolic_carbon=below_metabolic_carbon,
below_structural_carbon=below_structural_carbon,
above_metabolic_nitrogen=above_metabolic_nitrogen,
above_structural_nitrogen=above_structural_nitrogen,
woody_nitrogen=woody_nitrogen,
below_metabolic_nitrogen=below_metabolic_nitrogen,
below_structural_nitrogen=below_structural_nitrogen,
above_metabolic_phosphorus=above_metabolic_phosphorus,
above_structural_phosphorus=above_structural_phosphorus,
woody_phosphorus=woody_phosphorus,
below_metabolic_phosphorus=below_metabolic_phosphorus,
below_structural_phosphorus=below_structural_phosphorus,
above_structural_lignin=above_structural_lignin,
woody_lignin=woody_lignin,
below_structural_lignin=below_structural_lignin,
N_mineralisation_rate=N_mineralisation_rate,
P_mineralisation_rate=P_mineralisation_rate,
)
[docs]
def calculate_carbon_pool_loss(
old_pool_size: NDArray[np.floating],
final_pool_size: NDArray[np.floating],
input_rate: NDArray[np.floating],
update_interval: float,
) -> NDArray[np.floating]:
"""Calculate the total loss of carbon from a specific litter pool.
New carbon is added over the update interval so this has to be accounted for in the
calculation of the loss.
Args:
old_pool_size: The size of the litter pool before the update [kg{C} m^-2].
final_pool_size: The size of the litter pool after the update [kg{C} m^-2].
input_rate: The rate of carbon input to the litter pool [kg{C} m^-2 day^-1].
update_interval: The time period over which the litter pools are updated [days].
Returns:
The total loss of carbon from the pool due to decay [kg{C} m^-2]
"""
return old_pool_size + (input_rate * update_interval) - final_pool_size
[docs]
def calculate_nutrient_pool_loss(
initial_pool_carbon: NDArray[np.floating],
initial_pool_nutrient: NDArray[np.floating],
carbon_loss: NDArray[np.floating],
input_rate_carbon: NDArray[np.floating],
input_rate_nutrient: NDArray[np.floating],
update_interval: float,
) -> NDArray[np.floating]:
"""Calculate the total nutrient loss from a specific litter pool.
The change in the litter pool carbon content is found using an analytic solution,
but we don't have a comparable solution for the litter chemistries. Instead we make
the assumption that older material will preferentially break down, so the initial
pool stoichiometry can be used to calculate the approximate rate of nutrient loss.
However, applying this assumption in the case where the total loss of carbon is
larger than the initial pool size would break stoichiometric balance. In this case,
we assume that the entire initial pool has decayed and the additional carbon loss
comes from the input. The nutrient losses are then calculated based on this assumed
split.
Args:
initial_pool_carbon: Amount of carbon in the litter pool before the update
[kg{C} m^-2]
initial_pool_nutrient: Amount of nutrient in the litter pool before the update
[kg{nutrient} m^-2]
carbon_loss: The total loss of carbon from the pool over the decay period
[kg{C} m^-2]
input_rate_carbon: The rate of carbon input to the litter pool
[kg{C} m^-2 day^-1]
input_rate_nutrient: The rate of nutrient input to the litter pool
[kg{nutrient} m^-2 day^-1]
update_interval: The time period over which the litter pools are updated [days].
Returns:
The total loss of nutrient from the pool due to decay [kg{nutrient} m^-2]
"""
# Find the fraction of the initial pool that has decayed, and the fraction of input
# that decays (if the initial pool isn't sufficient)
fraction_of_initial_pool_decayed = np.where(
carbon_loss > initial_pool_carbon, 1, carbon_loss / initial_pool_carbon
)
fraction_of_new_input_decayed = np.divide(
carbon_loss - initial_pool_carbon,
input_rate_carbon * update_interval,
out=np.zeros_like(carbon_loss, dtype=float),
where=(carbon_loss > initial_pool_carbon) & (input_rate_carbon != 0),
)
# Find total amount of nutrient added over the time period
input_nutrient = input_rate_nutrient * update_interval
return (
fraction_of_initial_pool_decayed * initial_pool_nutrient
+ fraction_of_new_input_decayed * input_nutrient
)
[docs]
def calculate_lignin_pool_loss(
initial_pool_size: NDArray[np.floating],
carbon_loss: NDArray[np.floating],
input_rate: NDArray[np.floating],
initial_lignin_proportion: NDArray[np.floating],
input_lignin_proportion: NDArray[np.floating],
update_interval: float,
) -> NDArray[np.floating]:
"""Calculate the total lignin loss from a specific litter pool.
The change in the litter pool carbon content is found using an analytic solution,
but we don't have a comparable solution for the litter chemistries. Instead we make
the assumption that older material will preferentially break down, so the initial
pool lignin proportion can be used to calculate the approximate rate of lignin loss.
However, applying this assumption in the case where the total loss of carbon is
larger than the initial pool size would lead to spontaneous loss or creation of
lignin. In this case, we assume that the entire initial pool has decayed and the
additional carbon loss comes from the input. The lignin losses are then calculated
based on this assumed split.
Args:
initial_pool_size: The size of the litter pool before the update [kg{C} m^-2].
carbon_loss: The total loss of carbon from the pool over the decay period
[kg{C} m^-2].
input_rate: The rate of carbon input to the litter pool [kg{C} m^-2 day^-1].
initial_lignin_proportion: The lignin proportion of the litter pool before the
update [kg{lignin C} kg{C}^-1]
input_lignin_proportion: The lignin proportion of the input to the litter
pool [kg{lignin C} kg{C}^-1]
update_interval: The time period over which the litter pools are updated [days].
Returns:
The total loss of lignin from the pool due to decay [kg{lignin C} m^-2]
"""
# Find the fraction of the initial pool that has decayed, and the fraction of input
# that decays (if the initial pool isn't sufficient)
fraction_of_initial_pool_decayed = np.where(
carbon_loss > initial_pool_size, 1, carbon_loss / initial_pool_size
)
fraction_of_new_input_decayed = np.divide(
carbon_loss - initial_pool_size,
input_rate * update_interval,
out=np.zeros_like(carbon_loss, dtype=float),
where=(carbon_loss > initial_pool_size) & (input_rate != 0),
)
# Then calculate the amount of nutrient there initially and added due to input
initial_lignin = initial_pool_size * initial_lignin_proportion
input_lignin = input_rate * update_interval * input_lignin_proportion
return (
fraction_of_initial_pool_decayed * initial_lignin
+ fraction_of_new_input_decayed * input_lignin
)