Source code for virtual_ecosystem.models.litter.carbon

"""The ``models.litter.carbon`` module  tracks the carbon content of the litter pools
for the Virtual Ecosystem. Pools are divided into above and below ground pools, with
below ground pools affected by both soil moisture and temperature, and above ground
pools just affected by soil surface temperatures. The pools are also divided based on
the recalcitrance of their inputs, dead wood is given a separate pool, and all other
inputs are divided between metabolic and structural pools. Recalcitrant litter contains
hard to break down compounds, principally lignin. The metabolic litter pool contains the
non-recalcitrant litter and so breaks down quickly. Whereas, the structural litter
contains the recalcitrant litter.

We consider 5 pools rather than 6, as it's not really possible to parametrise the below
ground dead wood pool. So, all dead wood gets included in the above ground woody litter
pool.
"""  # noqa: D205

import numpy as np
from numpy.typing import NDArray
from xarray import DataArray

from virtual_ecosystem.core.core_components import LayerStructure
from virtual_ecosystem.core.model_config import CoreConstants
from virtual_ecosystem.models.litter.chemistry import calculate_litter_chemistry_factor
from virtual_ecosystem.models.litter.env_factors import (
    calculate_environmental_factors,
)
from virtual_ecosystem.models.litter.inputs import LitterInputs
from virtual_ecosystem.models.litter.losses import LitterLosses
from virtual_ecosystem.models.litter.model_config import LitterConstants


[docs] def calculate_post_consumption_pools( above_metabolic: DataArray, above_structural: DataArray, woody: DataArray, below_metabolic: DataArray, below_structural: DataArray, consumption_above_metabolic: DataArray, consumption_above_structural: DataArray, consumption_woody: DataArray, consumption_below_metabolic: DataArray, consumption_below_structural: DataArray, cell_area: float, ) -> dict[str, DataArray]: """Calculates the size of the five litter pools after animal consumption. At present the Virtual Ecosystem gives animals priority for consumption of litter. And so only the litter not consumed by animals has a chance to decay. This is a major assumption that we may have to revisit in future. This function calculates the change for all three nutrients (carbon, nitrogen and phosphorus) simultaneously, and also converts the animal consumption into density units. Args: above_metabolic: Above ground metabolic litter pool [kg m^-2] above_structural: Above ground structural litter pool [kg m^-2] woody: The woody litter pool [kg m^-2] below_metabolic: Below ground metabolic litter pool [kg m^-2] below_structural: Below ground structural litter pool [kg m^-2] consumption_above_metabolic: Amount of above-ground metabolic litter that has been consumed by animals [kg] consumption_above_structural: Amount of above-ground structural litter that has been consumed by animals [kg] consumption_woody: Amount of woody litter that has been consumed by animals [kg] consumption_below_metabolic: Amount of below-ground metabolic litter that has been consumed by animals [kg] consumption_below_structural: Amount of below-ground structural litter that has been consumed by animals [kg] cell_area: The area of each cell. [m^2] Returns: A dictionary containing the size of each litter pool after the mass consumed by animals has been removed [kg m^-2]. """ return { "above_metabolic": above_metabolic - (consumption_above_metabolic / cell_area), "above_structural": above_structural - (consumption_above_structural / cell_area), "woody": woody - (consumption_woody / cell_area), "below_metabolic": below_metabolic - (consumption_below_metabolic / cell_area), "below_structural": below_structural - (consumption_below_structural / cell_area), }
[docs] def calculate_decay_rates( lignin_above_structural: NDArray[np.floating], lignin_woody: NDArray[np.floating], lignin_below_structural: NDArray[np.floating], air_temperatures: DataArray, soil_temperatures: DataArray, water_potentials: DataArray, layer_structure: LayerStructure, constants: LitterConstants, ) -> dict[str, NDArray[np.floating]]: """Calculate the decay rate for all five of the litter pools. Args: lignin_above_structural: Proportion of above ground structural pool which is lignin [kg{lignin C} kg{C}^-1] lignin_woody: Proportion of dead wood pool which is lignin [kg{lignin C} kg{C}^-1] lignin_below_structural: Proportion of below ground structural pool which is lignin [kg{lignin C} kg{C}^-1] air_temperatures: Air temperatures, for all above ground layers [Celsius] soil_temperatures: Soil temperatures, for all soil layers [Celsius] water_potentials: Water potentials, for all soil layers [kPa] layer_structure: The LayerStructure instance for the simulation. constants: Set of constants for the litter model Decay rates depend on lignin proportions as well as a range of environmental factors. These environmental factors are calculated as part of this function. Returns: A dictionary containing the decay rate for each of the five litter pools. """ # Calculate environmental factors env_factors = calculate_environmental_factors( air_temperatures=air_temperatures, soil_temperatures=soil_temperatures, water_potentials=water_potentials, layer_structure=layer_structure, constants=constants, ) # Calculate decay rate for each pool metabolic_above_decay = calculate_litter_decay_metabolic_above( temperature_factor=env_factors["temp_above"], litter_decay_coefficient=constants.litter_decay_constant_metabolic_above, ) structural_above_decay = calculate_litter_decay_structural_above( temperature_factor=env_factors["temp_above"], lignin_proportion=lignin_above_structural, litter_decay_coefficient=constants.litter_decay_constant_structural_above, lignin_inhibition_factor=constants.lignin_inhibition_factor, ) woody_decay = calculate_litter_decay_woody( temperature_factor=env_factors["temp_above"], lignin_proportion=lignin_woody, litter_decay_coefficient=constants.litter_decay_constant_woody, lignin_inhibition_factor=constants.lignin_inhibition_factor, ) metabolic_below_decay = calculate_litter_decay_metabolic_below( temperature_factor=env_factors["temp_below"], moisture_factor=env_factors["water"], litter_decay_coefficient=constants.litter_decay_constant_metabolic_below, ) structural_below_decay = calculate_litter_decay_structural_below( temperature_factor=env_factors["temp_below"], moisture_factor=env_factors["water"], lignin_proportion=lignin_below_structural, litter_decay_coefficient=constants.litter_decay_constant_structural_below, lignin_inhibition_factor=constants.lignin_inhibition_factor, ) # Then return all the decay rates in a dictionary return { "metabolic_above": metabolic_above_decay, "structural_above": structural_above_decay, "woody": woody_decay, "metabolic_below": metabolic_below_decay, "structural_below": structural_below_decay, }
[docs] def calculate_total_C_mineralised( litter_losses: LitterLosses, model_constants: LitterConstants, core_constants: CoreConstants, update_interval: float, ) -> NDArray[np.floating]: """Calculate the total carbon mineralisation rate from all five litter pools. Args: litter_losses: Dataclass containing the total nutrient loss from each litter pool model_constants: Set of constants for the litter model core_constants: Set of core constants shared between all models update_interval: Interval that the litter pools are being updated for [days] Returns: Rate of carbon mineralisation from litter into soil [kg{C} m^-3 day^-1]. """ # Calculate mineralisation from each pool metabolic_above_mineral = calculate_carbon_mineralised( carbon_loss=litter_losses.above_metabolic_carbon, carbon_use_efficiency=model_constants.cue_metabolic, ) structural_above_mineral = calculate_carbon_mineralised( carbon_loss=litter_losses.above_structural_carbon, carbon_use_efficiency=model_constants.cue_structural_above_ground, ) woody_mineral = calculate_carbon_mineralised( carbon_loss=litter_losses.woody_carbon, carbon_use_efficiency=model_constants.cue_woody, ) metabolic_below_mineral = calculate_carbon_mineralised( carbon_loss=litter_losses.below_metabolic_carbon, carbon_use_efficiency=model_constants.cue_metabolic, ) structural_below_mineral = calculate_carbon_mineralised( carbon_loss=litter_losses.below_structural_carbon, carbon_use_efficiency=model_constants.cue_structural_below_ground, ) # Calculate mineralisation rate total_C_mineralised = ( metabolic_above_mineral + structural_above_mineral + woody_mineral + metabolic_below_mineral + structural_below_mineral ) # Convert total mineralisation rate into kg m^-3 day^-1 units (from kg m^-2) return total_C_mineralised / ( core_constants.max_depth_of_microbial_activity * update_interval )
[docs] def calculate_updated_pools( post_consumption_pools: dict[str, DataArray], decay_rates: dict[str, NDArray[np.floating]], litter_inputs: LitterInputs, update_interval: float, ) -> dict[str, NDArray[np.floating]]: """Calculate the updated mass of each litter pool. This function is not intended to be used continuously, and returns the new value for each pool after the update interval, rather than a rate of change to be integrated. Args: post_consumption_pools: The five litter pools after animal consumption has been subtracted [kg{C} m^-2] decay_rates: Dictionary containing the rates of decay for all 5 litter pools [kg{C} m^-2 day^-1] 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. update_interval: Interval that the litter pools are being updated for [days] constants: Set of constants for the litter model Returns: Dictionary containing the updated pool densities for all 5 litter pools (above ground metabolic, above ground structural, dead wood, below ground metabolic, and below ground structural) [kg{C} m^-2] """ return { "above_metabolic": calculate_final_pool_size( input_rate=litter_inputs.above_metabolic.to_numpy(), decay_rate=decay_rates["metabolic_above"], initial_pool=post_consumption_pools["above_metabolic"] .sel(element="C") .to_numpy(), update_interval=update_interval, ), "above_structural": calculate_final_pool_size( input_rate=litter_inputs.above_structural.to_numpy(), decay_rate=decay_rates["structural_above"], initial_pool=post_consumption_pools["above_structural"] .sel(element="C") .to_numpy(), update_interval=update_interval, ), "woody": calculate_final_pool_size( input_rate=litter_inputs.woody.to_numpy(), decay_rate=decay_rates["woody"], initial_pool=post_consumption_pools["woody"].sel(element="C").to_numpy(), update_interval=update_interval, ), "below_metabolic": calculate_final_pool_size( input_rate=litter_inputs.below_metabolic.to_numpy(), decay_rate=decay_rates["metabolic_below"], initial_pool=post_consumption_pools["below_metabolic"] .sel(element="C") .to_numpy(), update_interval=update_interval, ), "below_structural": calculate_final_pool_size( input_rate=litter_inputs.below_structural.to_numpy(), decay_rate=decay_rates["structural_below"], initial_pool=post_consumption_pools["below_structural"] .sel(element="C") .to_numpy(), update_interval=update_interval, ), }
[docs] def calculate_final_pool_size( input_rate: NDArray[np.floating], decay_rate: NDArray[np.floating], initial_pool: NDArray[np.floating], update_interval: float, ): """Calculate the final size of a litter pool based on input and decay rates. This function use an exact solution to the litter input and decay dynamics to find the pool size at the end of the update interval. This involves finding the equilibrium pool size based on the ratio of the input rate to the decay rate. The actual pool size exponentially decays from its initial size towards this equilibrium size with at the litter decay rate. Args: input_rate: The rate of input of carbon to the new pool [kg{C} m^-2 day^-1] decay_rate: The rate at which the pool decays (in carbon terms) [kg{C} m^-2 day^-1] initial_pool: The size of the pool at the start of the update interva [kg{C} m^-2] update_interval: Interval that the litter pools are being updated for [days] Returns: The size of the pool at the end of the time step [kg{C} m^-2] """ equilibrium_pool = input_rate / decay_rate return equilibrium_pool - (equilibrium_pool - initial_pool) * np.exp( -decay_rate * update_interval )
[docs] def calculate_litter_decay_metabolic_above( temperature_factor: NDArray[np.floating], litter_decay_coefficient: float, ) -> NDArray[np.floating]: """Calculate decay of above ground metabolic litter pool. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature_factor: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] litter_decay_coefficient: The decay coefficient for the above ground metabolic litter pool [day^-1] Returns: Rate of decay of the above ground metabolic litter pool [kg{C} m^-2 day^-1] """ return litter_decay_coefficient * temperature_factor
[docs] def calculate_litter_decay_structural_above( temperature_factor: NDArray[np.floating], lignin_proportion: NDArray[np.floating], litter_decay_coefficient: float, lignin_inhibition_factor: float, ) -> NDArray[np.floating]: """Calculate decay of above ground structural litter pool. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature_factor: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] lignin_proportion: The proportion of the above ground structural pool which is lignin [kg{lignin C} kg{C}^-1] litter_decay_coefficient: The decay coefficient for the above ground structural litter pool [day^-1] lignin_inhibition_factor: An exponential factor expressing the extent to which lignin inhibits the breakdown of litter [unitless] Returns: Rate of decay of the above ground structural litter pool [kg{C} m^-2 day^-1] """ litter_chemistry_factor = calculate_litter_chemistry_factor( lignin_proportion, lignin_inhibition_factor=lignin_inhibition_factor ) return litter_decay_coefficient * temperature_factor * litter_chemistry_factor
[docs] def calculate_litter_decay_woody( temperature_factor: NDArray[np.floating], lignin_proportion: NDArray[np.floating], litter_decay_coefficient: float, lignin_inhibition_factor: float, ) -> NDArray[np.floating]: """Calculate decay of the woody litter pool. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature_factor: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] lignin_proportion: The proportion of the woody litter pool which is lignin [kg{lignin C} kg{C}^-1] litter_decay_coefficient: The decay coefficient for the woody litter pool [day^-1] lignin_inhibition_factor: An exponential factor expressing the extent to which lignin inhibits the breakdown of litter [unitless] Returns: Rate of decay of the woody litter pool [kg{C} m^-2 day^-1] """ litter_chemistry_factor = calculate_litter_chemistry_factor( lignin_proportion, lignin_inhibition_factor=lignin_inhibition_factor ) return litter_decay_coefficient * temperature_factor * litter_chemistry_factor
[docs] def calculate_litter_decay_metabolic_below( temperature_factor: NDArray[np.floating], moisture_factor: NDArray[np.floating], litter_decay_coefficient: float, ) -> NDArray[np.floating]: """Calculate decay of below ground metabolic litter pool. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature_factor: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] moisture_factor: A multiplicative factor capturing the impact of soil moisture on litter decomposition [unitless] litter_decay_coefficient: The decay coefficient for the below ground metabolic litter pool [day^-1] Returns: Rate of decay of the below ground metabolic litter pool [kg{C} m^-2 day^-1] """ return litter_decay_coefficient * temperature_factor * moisture_factor
[docs] def calculate_litter_decay_structural_below( temperature_factor: NDArray[np.floating], moisture_factor: NDArray[np.floating], lignin_proportion: NDArray[np.floating], litter_decay_coefficient: float, lignin_inhibition_factor: float, ) -> NDArray[np.floating]: """Calculate decay of below ground structural litter pool. This function is taken from :cite:t:`kirschbaum_modelling_2002`. Args: temperature_factor: A multiplicative factor capturing the impact of temperature on litter decomposition [unitless] moisture_factor: A multiplicative factor capturing the impact of soil moisture on litter decomposition [unitless] lignin_proportion: The proportion of the below ground structural pool which is lignin [kg{lignin C} kg{C}^-1] litter_decay_coefficient: The decay coefficient for the below ground structural litter pool [day^-1] lignin_inhibition_factor: An exponential factor expressing the extent to which lignin inhibits the breakdown of litter [unitless] Returns: Rate of decay of the below ground structural litter pool [kg{C} m^-2 day^-1] """ litter_chemistry_factor = calculate_litter_chemistry_factor( lignin_proportion, lignin_inhibition_factor=lignin_inhibition_factor ) return ( litter_decay_coefficient * temperature_factor * moisture_factor * litter_chemistry_factor )
[docs] def calculate_carbon_mineralised( carbon_loss: NDArray[np.floating], carbon_use_efficiency: float ) -> NDArray[np.floating]: """Calculate fraction of carbon loss that gets mineralised. TODO - This function could also be used to track carbon respired, if/when we decide to track that. Args: carbon_loss: Total amount of carbon lost from the litter pool [kg{C} m^-2] carbon_use_efficiency: Carbon use efficiency of litter pool [unitless] Returns: Rate at which carbon is mineralised from the litter pool [kg{C} m^-2] """ return carbon_use_efficiency * carbon_loss