Source code for virtual_ecosystem.models.litter.inputs
"""The ``models.litter.inputs`` module handles the partitioning of plant matter into the
various pools of the litter model. This plant matter comes from both natural tissue
death as well as from mechanical inefficiencies in herbivory.
""" # noqa: D205
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from numpy.typing import NDArray
from xarray import DataArray, zeros_like
from virtual_ecosystem.core.data import Data
from virtual_ecosystem.core.logger import LOGGER
from virtual_ecosystem.models.litter.model_config import LitterConstants
[docs]
@dataclass(frozen=True)
class LitterInputs:
"""The full set input flows to the litter model."""
leaf_mass: DataArray
"""Total leaf input rate to litter of each nutrient [kg m^-2 day^-1]"""
root_mass: DataArray
"""Total root input rate to litter of each nutrient [kg m^-2 day^-1]"""
deadwood_mass: DataArray
"""Total deadwood input rate to litter of each nutrient [kg m^-2 day^-1]"""
leaf_lignin: DataArray
"""Lignin proportion of leaf input [kg{lignin C} kg{C}^-1]"""
root_lignin: DataArray
"""Lignin proportion of root input [kg{lignin C} kg{C}^-1]"""
stem_lignin: DataArray
"""Lignin proportion of deadwood input [kg{lignin C} kg{C}^-1]"""
leaves_meta_split: DataArray
"""Fraction of leaf input that goes to metabolic litter [unitless]"""
roots_meta_split: DataArray
"""Fraction of leaf input that goes to metabolic litter [unitless]"""
woody: DataArray
"""Total input rate to the woody litter pool [kg{C} m^-2 day^-1]"""
above_metabolic: DataArray
"""Total input rate to the above ground metabolic litter pool [kg{C} m^-2 day^-1]"""
above_structural: DataArray
"""Total input rate to the above ground structural litter pool [kg{C} m^-2 day^-1]
"""
below_metabolic: DataArray
"""Total input rate to the below ground metabolic litter pool [kg{C} m^-2 day^-1]"""
below_structural: DataArray
"""Total input rate to the below ground structural litter pool [kg{C} m^-2 day^-1]
"""
[docs]
@classmethod
def create_from_data(
cls, data: Data, constants: LitterConstants, update_interval: float
) -> LitterInputs:
"""Factory method to populate the various litter input flows.
This method first combines the two different input streams for dead plant matter
(plant tissue death and herbivory waste) to find the total input of each plant
biomass type. This is then used to find the split between metabolic and
structural litter pools for each plant matter class (expect deadwood). Finally,
the total rate of flow to each litter pool is calculated.
Args:
data: The `Data` object to be used to populate the litter input details.
constants: Set of constants for the litter model.
update_interval: The length of time over which the input is being added over
[days]
Returns:
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.
"""
# Find the total input for each plant matter type
total_input = combine_input_sources(data, update_interval=update_interval)
# Find the plant inputs to each of the litter pools
metabolic_splits = calculate_metabolic_proportions_of_input(
total_input=total_input, constants=constants
)
plant_inputs = partion_plant_inputs_between_pools(
total_input=total_input,
metabolic_splits=metabolic_splits,
)
return LitterInputs(**metabolic_splits, **plant_inputs, **total_input)
[docs]
def convert_to_input_masses_to_rates_per_area(
input_mass: DataArray, cell_area: float, update_interval: float
) -> DataArray:
"""Helper function to convert input masses to rates per area.
The plant model stores plant biomass in units of mass (kg) per grid square,
whereas in the litter model we need everything as input rates per area.
Args:
input_mass: The mass of the input [kg]
cell_area: The size of the grid cell [m^2]
update_interval: The length of time over which the input is being added over
[days]
Returns:
The input as a per area input rate [kg m^-2 day^-1]
"""
return input_mass / (cell_area * update_interval)
[docs]
def combine_input_sources(data: Data, update_interval: float) -> dict[str, DataArray]:
"""Combine the plant death and herbivory inputs into a single total input.
The total input for each plant matter type (leaves, roots, deadwood) is returned,
the chemical concentration of each of these new pools is also calculated.
This function also converts the plant inputs from total inputs (over the model time
step), to the (per area) input rates needed by the litter model.
Args:
data: The `Data` object to be used to populate the litter input streams.
update_interval: The length of time over which the input is being added over
[days]
Returns:
A dictionary containing the combined rate at which each input pools is added to
[kg{C} m^-2 day^-1], as well as the carbon to nitrogen ratios [unitless], carbon
to phosphorus ratios [unitless] and lignin proportions [kg{lignin C} kg{C}^-1]
of each of these pools.
"""
# Calculate totals for each plant matter type, collapsing PFTs for leaf inputs
leaf_rates = convert_to_input_masses_to_rates_per_area(
data["foliage_turnover_cnp"].sum(dim="pft") + data["herbivory_waste_leaf_cnp"],
cell_area=data.grid.cell_area,
update_interval=update_interval,
)
root_rates = convert_to_input_masses_to_rates_per_area(
data["root_turnover_cnp"],
cell_area=data.grid.cell_area,
update_interval=update_interval,
)
deadwood_rates = convert_to_input_masses_to_rates_per_area(
data["stem_turnover_cnp"],
cell_area=data.grid.cell_area,
update_interval=update_interval,
)
# Calculate lignin concentrations for each combined pool
foliage_mass = data["foliage_turnover_cnp"].sel(element="C").sum(dim="pft")
leaf_lignin = merge_input_lignin_proportions(
turnover_mass=foliage_mass,
herbivory_waste_mass=data["herbivory_waste_leaf_cnp"].sel(element="C"),
total_mass=(foliage_mass + data["herbivory_waste_leaf_cnp"].sel(element="C")),
turnover_lignin_proportion=data["senesced_leaf_lignin"],
herbivory_waste_lignin_proportion=data["herbivory_waste_leaf_lignin"],
)
root_lignin = data["root_lignin"]
stem_lignin = data["stem_lignin"]
return {
"leaf_mass": leaf_rates,
"deadwood_mass": deadwood_rates,
"root_mass": root_rates,
"leaf_lignin": leaf_lignin,
"root_lignin": root_lignin,
"stem_lignin": stem_lignin,
}
[docs]
def calculate_metabolic_proportions_of_input(
total_input: dict[str, DataArray], constants: LitterConstants
) -> dict[str, DataArray]:
"""Calculate the proportion of each input type that flows to the metabolic pool.
This function is used for roots, leaves and reproductive tissue, but not deadwood
because everything goes into a single woody litter pool. It is not used for animal
inputs either as they all flow into just the metabolic pool.
Args:
total_input: The total amount of carbon in each input pools [kg{C} m^-3], as
well as the nitrogen content [kg{N} m^-3], phosphorus content [kg{P} m^-3]
and lignin proportions [kg{lignin C} kg{C}^-1] of each of these pools.
constants: Set of constants for the litter model.
Returns:
A dictionary containing the proportion of the input that goes to the relevant
metabolic pool. This is for three input types: leaves, reproductive tissues and
roots [unitless]
"""
# Calculate split of each input biomass type
leaves_metabolic_split = split_pool_into_metabolic_and_structural_litter(
input_masses=total_input["leaf_mass"],
lignin_proportion=total_input["leaf_lignin"],
max_metabolic_fraction=constants.max_metabolic_fraction_of_input,
split_sensitivity_nitrogen=constants.metabolic_split_nitrogen_sensitivity,
split_sensitivity_phosphorus=constants.metabolic_split_phosphorus_sensitivity,
)
roots_metabolic_split = split_pool_into_metabolic_and_structural_litter(
input_masses=total_input["root_mass"],
lignin_proportion=total_input["root_lignin"],
max_metabolic_fraction=constants.max_metabolic_fraction_of_input,
split_sensitivity_nitrogen=constants.metabolic_split_nitrogen_sensitivity,
split_sensitivity_phosphorus=constants.metabolic_split_phosphorus_sensitivity,
)
return {
"leaves_meta_split": leaves_metabolic_split,
"roots_meta_split": roots_metabolic_split,
}
[docs]
def partion_plant_inputs_between_pools(
total_input: dict[str, DataArray],
metabolic_splits: dict[str, DataArray],
) -> dict[str, DataArray]:
"""Function to partition input biomass between the various litter pools.
All deadwood is added to the woody litter pool. Leaf biomass is split between the
above ground metabolic and structural pools based on lignin concentration and carbon
nitrogen ratios. Root biomass is split between the below ground metabolic and
structural pools based on lignin concentration and carbon nitrogen ratios.
Args:
total_input: The the total pool size for each input pools [kg{C} m^-3], as
well as the carbon to nitrogen ratios [unitless], carbon to phosphorus
ratios [unitless] and lignin proportions [kg{lignin C} kg{C}^-1] of each of
these pools.
metabolic_splits: Dictionary containing the proportion of each input that
goes to the relevant metabolic pool. This is for three input types:
leaves, reproductive tissues and roots [unitless]
Returns:
A dictionary containing the rate of biomass flow into each of the five litter
pools (woody, above ground metabolic, above ground structural, below ground
metabolic and below ground structural) [kg{C} m^-2 day^-1]
"""
# Calculate input to each of the five litter pools
woody_input = total_input["deadwood_mass"].sel(element="C")
above_ground_metabolic_input = metabolic_splits["leaves_meta_split"] * total_input[
"leaf_mass"
].sel(element="C")
above_ground_strutural_input = (
1 - metabolic_splits["leaves_meta_split"]
) * total_input["leaf_mass"].sel(element="C")
below_ground_metabolic_input = metabolic_splits["roots_meta_split"] * total_input[
"root_mass"
].sel(element="C")
below_ground_structural_input = (
1 - metabolic_splits["roots_meta_split"]
) * total_input["root_mass"].sel(element="C")
return {
"woody": woody_input,
"above_metabolic": above_ground_metabolic_input,
"above_structural": above_ground_strutural_input,
"below_metabolic": below_ground_metabolic_input,
"below_structural": below_ground_structural_input,
}
[docs]
def split_pool_into_metabolic_and_structural_litter(
input_masses: DataArray,
lignin_proportion: DataArray,
max_metabolic_fraction: float,
split_sensitivity_nitrogen: float,
split_sensitivity_phosphorus: float,
):
"""Calculate the split of input biomass between metabolic and structural pools.
This division depends on the lignin, nitrogen and phosphorus contents of the input
biomass, the functional form is taken from :cite:t:`parton_dynamics_1988`.
Args:
input_masses: Mass of each nutrient in the input biomass [kg{C} m^-2]
lignin_proportion: Proportion of input biomass carbon that is lignin
[kg{lignin C} kg{C}^-1]
max_metabolic_fraction: Fraction of pool that becomes metabolic litter for the
easiest to breakdown case, i.e. no lignin, ample nitrogen [unitless]
split_sensitivity_nitrogen: Sets how rapidly the split changes in response to
changing lignin and nitrogen contents [unitless]
split_sensitivity_phosphorus: Sets how rapidly the split changes in response to
changing lignin and phosphorus contents [unitless]
Raises:
ValueError: If any values of lignin proportion are not between zero and one
(which breaks the logic of this function)
Returns:
The fraction of the biomass that goes to the metabolic pool [unitless]
"""
# First check that lignin proportions are between zero and one (if they aren't that
# screws up the logic)
if np.any((lignin_proportion < 0.0) | (lignin_proportion > 1.0)):
to_raise = ValueError("Lignin proportion not between 0 and 1 (inclusive)!")
LOGGER.error(to_raise)
raise to_raise
# Extract the specific nutrient masses from the summary
input_carbon = input_masses.sel(element="C").to_numpy()
input_nitrogen = input_masses.sel(element="N").to_numpy()
input_phosphorus = input_masses.sel(element="P").to_numpy()
# Calculate carbon:nitrogen and carbon:phosphorus ratios based on the input
# biomasses
carbon_nitrogen_ratio = np.divide(
input_carbon,
input_nitrogen,
out=np.full_like(input_carbon, np.inf, dtype=float),
where=input_nitrogen != 0,
)
carbon_phosphorus_ratio = np.divide(
input_carbon,
input_phosphorus,
out=np.full_like(input_carbon, np.inf, dtype=float),
where=input_phosphorus != 0,
)
metabolic_fraction = max_metabolic_fraction - lignin_proportion * (
split_sensitivity_nitrogen * carbon_nitrogen_ratio
+ split_sensitivity_phosphorus * carbon_phosphorus_ratio
)
# This is a naive prevention of negative metabolic fraction rates.
# TODO: full solution in Issue #1010.
metabolic_fraction = np.where(metabolic_fraction < 0, 0.0, metabolic_fraction)
# Another naive prevention of metabolic fractions that are so large that all lignin
# cannot flow to the structural pool
# TODO: full solution again in Issue #1010.
metabolic_fraction = np.where(
metabolic_fraction > 1 - lignin_proportion,
1 - lignin_proportion,
metabolic_fraction,
)
return metabolic_fraction
[docs]
def merge_input_lignin_proportions(
turnover_mass: DataArray,
herbivory_waste_mass: DataArray,
total_mass: DataArray,
turnover_lignin_proportion: DataArray,
herbivory_waste_lignin_proportion: DataArray,
) -> DataArray:
"""Merge the lignin proportions of two input sources to the same litter pool.
Args:
turnover_mass: Input mass coming from the natural turnover of plant tissue
[kg{C}]
herbivory_waste_mass: Input mass coming from the mechanical inefficiencies of
herbivory [kg{C}]
total_mass: The combined mass of the two input sources [kg{C}]
turnover_lignin_proportion: Proportion of lignin in the input mass from
natural plant turnover [unitless]
herbivory_waste_lignin_proportion: Proportion of lignin in the input mass from
mechanical inefficiencies of herbivory [unitless]
Returns:
The proportion of carbon that is lignin carbon in the total mass of the new
combined input stream [kg{lignin C} kg{C}^-1]
"""
return (
turnover_lignin_proportion * turnover_mass
+ herbivory_waste_lignin_proportion * herbivory_waste_mass
) / (total_mass)
[docs]
def average_nutrient_ratios(
mass_1: NDArray[np.floating],
mass_2: NDArray[np.floating],
nutrient_ratio_1: NDArray[np.floating],
nutrient_ratio_2: NDArray[np.floating],
):
"""Average carbon to nutrient ratios weighted by their carbon content.
Args:
mass_1: Total carbon mass of the first pool/input stream
[kg{C} m^-2 or kg{C} m^-2]
mass_2: Total carbon mass of the second pool/input stream
[kg{C} m^-2 or kg{C} m^-2]
nutrient_ratio_1: Carbon to nutrient ratio of the first pool/input stream
[unitless]
nutrient_ratio_2: Carbon to nutrient ratio of the second pool/input stream
[unitless]
Returns:
The nutrient ratio of the new combined pool/input stream [unitless]
"""
return (mass_1 + mass_2) / (
(mass_1 / nutrient_ratio_1) + (mass_2 / nutrient_ratio_2)
)
[docs]
@dataclass(frozen=True)
class InputChemistries:
"""Dataclass containing the chemistry for the input to each litter pool."""
above_metabolic_nitrogen: DataArray
"""Nitrogen input rate to the aboveground metabolic pool [kg{N} m^-2 day^-1]"""
above_structural_nitrogen: DataArray
"""Nitrogen input rate to the aboveground structural pool [kg{N} m^-2 day^-1]"""
woody_nitrogen: DataArray
"""Nitrogen input rate to the woody pool [kg{N} m^-2 day^-1]"""
below_metabolic_nitrogen: DataArray
"""Nitrogen input rate to the belowground metabolic pool [kg{N} m^-2 day^-1]"""
below_structural_nitrogen: DataArray
"""Nitrogen input rate to the belowground structural pool [kg{N} m^-2 day^-1]"""
above_metabolic_phosphorus: DataArray
"""Phosphorus input rate to the aboveground metabolic pool [kg{P} m^-2 day^-1]"""
above_structural_phosphorus: DataArray
"""Phosphorus input rate to the aboveground structural pool [kg{P} m^-2 day^-1]"""
woody_phosphorus: DataArray
"""Phosphorus input rate to the woody pool [kg{P} m^-2 day^-1]"""
below_metabolic_phosphorus: DataArray
"""Phosphorus input rate to the belowground metabolic pool [kg{P} m^-2 day^-1]"""
below_structural_phosphorus: DataArray
"""Phosphorus input rate to the belowground structural pool [kg{P} m^-2 day^-1]"""
above_structural_lignin: DataArray
"""Lignin proportion of input to the aboveground structural pool.
Units of [kg{lignin C} kg{C}^-1]
"""
woody_lignin: DataArray
"""Lignin proportion of input to the woody pool [kg{lignin C} kg{C}^-1]"""
below_structural_lignin: DataArray
"""Lignin proportion of input to the belowground structural pool.
Units of [kg{lignin C} kg{C}^-1]
"""
[docs]
def calculate_input_chemistries(
litter_inputs: LitterInputs,
meta_to_struct_nitrogen_ratio: float,
meta_to_struct_phosphorus_ratio: float,
) -> InputChemistries:
"""Calculate the chemistries of the input to each litter pool.
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.
meta_to_struct_nitrogen_ratio: Ratio of nitrogen concentrations for inputs to
the metabolic and structural litter pools [unitless]
meta_to_struct_phosphorus_ratio: Ratio of phosphorus concentrations for inputs
to the metabolic and structural litter pools [unitless]
Returns:
An InputChemistries instance containing the input chemistry for each litter pool
"""
# Find lignin and nitrogen contents of the litter input flows
input_lignin = calculate_litter_input_lignin_concentrations(
litter_inputs=litter_inputs,
)
input_c_n_ratios = calculate_litter_input_nutrient_masses(
litter_inputs=litter_inputs,
meta_to_struct_nutrient_ratio=meta_to_struct_nitrogen_ratio,
nutrient="nitrogen",
)
input_c_p_ratios = calculate_litter_input_nutrient_masses(
litter_inputs=litter_inputs,
meta_to_struct_nutrient_ratio=meta_to_struct_phosphorus_ratio,
nutrient="phosphorus",
)
return InputChemistries(**input_lignin, **input_c_n_ratios, **input_c_p_ratios)
[docs]
def calculate_litter_input_lignin_concentrations(
litter_inputs: LitterInputs,
) -> dict[str, DataArray]:
"""Calculate the concentration of lignin for each plant biomass to litter flow.
By definition the metabolic litter pools do not contain lignin, so all input
lignin flows to the structural and woody pools. As the input biomass gets split
between pools, the lignin concentration of the input to the structural pools
will be higher than it was in the input biomass.
For the woody litter there's no structural-metabolic split so the lignin
concentration of the litter input is the same as that of the dead wood
production. For the below ground structural litter, the total lignin content of
root input must be found, this is then converted back into a concentration
relative to the input into the below structural litter pool. For the above
ground structural litter pool, the same approach is taken using leaf lignin.
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.
Returns:
Dictionary containing the lignin concentration of the input to each of the
three lignin containing litter pools (woody, above and below ground
structural) [kg{lignin C} kg{C}^-1]
"""
lignin_proportion_woody = litter_inputs.stem_lignin
lignin_proportion_below_structural = (
litter_inputs.root_lignin
* litter_inputs.root_mass.sel(element="C")
/ litter_inputs.below_structural
)
lignin_proportion_above_structural = (
litter_inputs.leaf_lignin
* litter_inputs.leaf_mass.sel(element="C")
/ litter_inputs.above_structural
)
return {
"woody_lignin": lignin_proportion_woody,
"below_structural_lignin": lignin_proportion_below_structural,
"above_structural_lignin": lignin_proportion_above_structural,
}
[docs]
def calculate_litter_input_nutrient_masses(
litter_inputs: LitterInputs, meta_to_struct_nutrient_ratio: float, nutrient: str
) -> dict[str, DataArray]:
"""Calculate the nutrient mass for each plant biomass to litter flow.
Input to the woody litter pool just matches the nutrient masses of the
deadwood input. For the other pools, the split of nutrient flows from root/leaf
turnover between the metabolic and structural pools is calculated.
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.
meta_to_struct_nutrient_ratio: Ratio of the nutrient concentrations (relative to
carbon mass) for the inputs to the metabolic and structural litter pools
[unitless]
nutrient: Nutrient to calculate flows for (options are nitrogen and phosphorus)
Returns:
Dictionary containing input rates of the nutrients (nitrogen or phosphorus) into
each of the pools [kg{nutrient} m^-2 day^-1]
"""
if nutrient not in {"nitrogen", "phosphorus"}:
to_raise = ValueError(f"{nutrient} is not an element we currently track!")
LOGGER.error(to_raise)
raise to_raise
# TODO - This will need a more sophisticated approach if we start tracking potassium
element_symbol = nutrient[0].upper()
# Calculate nutrient split for each (non-wood) input biomass type
root_nutrient_meta, root_nutrient_struct = find_nutrient_split_between_litter_pools(
input_carbon_rate=litter_inputs.root_mass.sel(element="C"),
input_nutrient_rate=litter_inputs.root_mass.sel(element=element_symbol),
metabolic_split=litter_inputs.roots_meta_split,
meta_to_struct_nutrient_ratio=meta_to_struct_nutrient_ratio,
)
leaf_nutrient_meta, leaf_nutrient_struct = find_nutrient_split_between_litter_pools(
input_carbon_rate=litter_inputs.leaf_mass.sel(element="C"),
input_nutrient_rate=litter_inputs.leaf_mass.sel(element=element_symbol),
metabolic_split=litter_inputs.leaves_meta_split,
meta_to_struct_nutrient_ratio=meta_to_struct_nutrient_ratio,
)
return {
f"woody_{nutrient}": litter_inputs.deadwood_mass.sel(element=element_symbol),
f"below_metabolic_{nutrient}": root_nutrient_meta,
f"below_structural_{nutrient}": root_nutrient_struct,
f"above_metabolic_{nutrient}": leaf_nutrient_meta,
f"above_structural_{nutrient}": leaf_nutrient_struct,
}
[docs]
def find_nutrient_split_between_litter_pools(
input_carbon_rate: DataArray,
input_nutrient_rate: DataArray,
metabolic_split: DataArray,
meta_to_struct_nutrient_ratio: float,
) -> tuple[DataArray, DataArray]:
"""Function to find the split of input nutrients between litter pools.
Following :cite:t:`kirschbaum_modelling_2002`, we assume that the nutrient
concentrations of inputs to the structural and metabolic litter pools are in a fixed
proportion. This ratio can vary between nutrients but doesn't vary between above and
below ground pools. This simplifying assumption allows us to capture litter
mineralisation dynamics for specific elements without having to build (and
parametrise) a model where every nutrient effects decay rate of every pool.
This function explicitly handle the edge case where there is no carbon flow to
structural litter. How this is done depends on whether there is a carbon flow to
metabolic litter. If there is, then all nutrient flow goes to the metabolic litter.
If there isn't, then there is no nutrient flow to either pool.
Args:
input_carbon_rate: Rate of carbon input [kg{C} m^-2 day^-1]
input_nutrient_rate: Rate of nutrient input [kg{nutrient} m^-2 day^-1]
metabolic_split: Proportion of organic matter input that flows to the metabolic
litter pool [unitless]
meta_to_struct_nutrient_ratio: Ratio of the nutrient concentrations (relative to
carbon mass) for the inputs to the metabolic and structural litter pools
[unitless]
Returns:
A tuple containing the nutrient input rate to the metabolic and structural
litter pools [kg{nutrient} m^-2 day^-1], in that order.
"""
# Calculate rate of carbon input to each pool
carbon_input_meta = input_carbon_rate * metabolic_split
carbon_input_struct = input_carbon_rate * (1 - metabolic_split)
meta_nutrient_mass = zeros_like(carbon_input_meta)
struct_nutrient_mass = zeros_like(carbon_input_struct)
# Defining masks to avoid zero flow edge cases
struct_input = carbon_input_struct != 0
only_meta_input = (carbon_input_meta != 0) & (carbon_input_struct == 0)
# Calculate actual split in cases where there is flow to the structural pool
meta_nutrient, struct_nutrient = calculate_nutrient_split(
carbon_input_meta.where(struct_input),
carbon_input_struct.where(struct_input),
input_nutrient_rate.where(struct_input),
meta_to_struct_nutrient_ratio,
)
# Zero values get replaced if there is a flow to that pool (otherwise kept as zero)
meta_nutrient_mass = meta_nutrient_mass.where(~struct_input, other=meta_nutrient)
struct_nutrient_mass = struct_nutrient_mass.where(
~struct_input, other=struct_nutrient
)
# If all carbon goes to metabolic litter all nutrients do as well
meta_nutrient_mass = meta_nutrient_mass.where(
~only_meta_input, input_nutrient_rate.where(only_meta_input)
)
return (meta_nutrient_mass, struct_nutrient_mass)
[docs]
def calculate_nutrient_split(
carbon_input_meta: DataArray,
carbon_input_struct: DataArray,
input_nutrient_rate: DataArray,
meta_to_struct_nutrient_ratio: float,
) -> tuple[DataArray, DataArray]:
"""Calculate the split of nutrients between metabolic and structural litter pools.
The calculation of this split derives from :cite:t:`kirschbaum_modelling_2002`, and
assumes that the nutrient concentrations of inputs to the structural and metabolic
litter pools are in a fixed proportion.
Args:
carbon_input_meta: Rate of carbon input to the metabolic pool
[kg{C} m^-2 day^-1]
carbon_input_struct: Rate of carbon input to the structural pool
[kg{C} m^-2 day^-1]
input_nutrient_rate: Total rate of nutrient input [kg{nutrient} m^-2 day^-1]
meta_to_struct_nutrient_ratio: Ratio of the nutrient concentrations (relative to
carbon mass) for the inputs to the metabolic and structural litter pools
[unitless]
Returns:
A tuple containing the rate of nutrient flow to the metabolic and structural
litter pools, respectively [kg{nutrient} m^-2 day^-1]
"""
# Find ratio of the carbon contents of the two pools
ratio_meta_to_struct = carbon_input_meta / carbon_input_struct
# The product of the ratio of carbon inputs and the ratio of the nutrient
# concentrations is found and used to calculate the nutrient mass in each litter
# pool
product_of_ratios = ratio_meta_to_struct * meta_to_struct_nutrient_ratio
struct_nutrient_mass = input_nutrient_rate / (1 + product_of_ratios)
meta_nutrient_mass = product_of_ratios * struct_nutrient_mass
return meta_nutrient_mass, struct_nutrient_mass