diff --git a/docs/source/_toc.yaml b/docs/source/_toc.yaml index 07741f0e8..d81372afd 100644 --- a/docs/source/_toc.yaml +++ b/docs/source/_toc.yaml @@ -183,8 +183,8 @@ subtrees: title: The constants submodule - file: api/models/litter/env_factors title: The env_factors submodule - - file: api/models/litter/input_partition - title: The input_partition submodule + - file: api/models/litter/inputs + title: The inputs submodule - file: api/models/litter/litter_model title: The litter_model submodule - file: api/models/soil diff --git a/docs/source/api/models/litter/input_partition.md b/docs/source/api/models/litter/inputs.md similarity index 81% rename from docs/source/api/models/litter/input_partition.md rename to docs/source/api/models/litter/inputs.md index 8dbbf4e4d..c5f045cfc 100644 --- a/docs/source/api/models/litter/input_partition.md +++ b/docs/source/api/models/litter/inputs.md @@ -14,10 +14,10 @@ kernelspec: name: python3 --- -# API documentation for the {mod}`~virtual_ecosystem.models.litter.input_partition` module +# API documentation for the {mod}`~virtual_ecosystem.models.litter.inputs` module ```{eval-rst} -.. automodule:: virtual_ecosystem.models.litter.input_partition +.. automodule:: virtual_ecosystem.models.litter.inputs :autosummary: :members: ``` diff --git a/tests/models/animals/test_animal_model.py b/tests/models/animals/test_animal_model.py index 9ea7ffe6f..096a7ee47 100644 --- a/tests/models/animals/test_animal_model.py +++ b/tests/models/animals/test_animal_model.py @@ -180,6 +180,10 @@ def test_animal_model_initialization( (INFO, "Adding data array for 'litter_consumption_woody'"), (INFO, "Adding data array for 'litter_consumption_below_metabolic'"), (INFO, "Adding data array for 'litter_consumption_below_structural'"), + (INFO, "Adding data array for 'herbivory_waste_leaf_carbon'"), + (INFO, "Adding data array for 'herbivory_waste_leaf_nitrogen'"), + (INFO, "Adding data array for 'herbivory_waste_leaf_phosphorus'"), + (INFO, "Adding data array for 'herbivory_waste_leaf_lignin'"), ), id="success", ), diff --git a/tests/models/litter/conftest.py b/tests/models/litter/conftest.py index c0bf89959..c37829fd4 100644 --- a/tests/models/litter/conftest.py +++ b/tests/models/litter/conftest.py @@ -80,6 +80,10 @@ def dummy_litter_data(fixture_core_components): "litter_consumption_woody": [0.4773833, 0.385701, 0.373456, 0.162192], "litter_consumption_below_metabolic": [0.010373, 0.005794, 0.010181, 0.013494], "litter_consumption_below_structural": [0.013547, 0.011674, 0.012738, 0.009168], + "herbivory_waste_leaf_carbon": [3e-5, 2.1e-3, 2.85e-3, 2.7e-3], + "herbivory_waste_leaf_nitrogen": [23.1, 33.5, 23.1, 17.3], + "herbivory_waste_leaf_phosphorus": [212.5, 344.8, 334.8, 420.1], + "herbivory_waste_leaf_lignin": [0.13, 0.08, 0.27, 0.22], } for var, vals in pool_values.items(): @@ -125,59 +129,6 @@ def decay_rates(dummy_litter_data, fixture_core_components, post_consumption_poo return decay_rates -@pytest.fixture -def metabolic_splits(dummy_litter_data): - """Metabolic splits for the various plant inputs.""" - - from virtual_ecosystem.models.litter.input_partition import ( - calculate_metabolic_proportions_of_input, - ) - - metabolic_splits = calculate_metabolic_proportions_of_input( - leaf_turnover_lignin_proportion=dummy_litter_data[ - "leaf_turnover_lignin" - ].to_numpy(), - reproduct_turnover_lignin_proportion=dummy_litter_data[ - "plant_reproductive_tissue_turnover_lignin" - ].to_numpy(), - root_turnover_lignin_proportion=dummy_litter_data[ - "root_turnover_lignin" - ].to_numpy(), - leaf_turnover_c_n_ratio=dummy_litter_data["leaf_turnover_c_n_ratio"].to_numpy(), - reproduct_turnover_c_n_ratio=dummy_litter_data[ - "plant_reproductive_tissue_turnover_c_n_ratio" - ].to_numpy(), - root_turnover_c_n_ratio=dummy_litter_data["root_turnover_c_n_ratio"].to_numpy(), - leaf_turnover_c_p_ratio=dummy_litter_data["leaf_turnover_c_p_ratio"].to_numpy(), - reproduct_turnover_c_p_ratio=dummy_litter_data[ - "plant_reproductive_tissue_turnover_c_p_ratio" - ].to_numpy(), - root_turnover_c_p_ratio=dummy_litter_data["root_turnover_c_p_ratio"].to_numpy(), - constants=LitterConsts, - ) - - return metabolic_splits - - -@pytest.fixture -def plant_inputs(dummy_litter_data, metabolic_splits): - """Plant inputs to each of the litter pools.""" - - from virtual_ecosystem.models.litter.input_partition import ( - partion_plant_inputs_between_pools, - ) - - plant_inputs = partion_plant_inputs_between_pools( - deadwood_production=dummy_litter_data["deadwood_production"], - leaf_turnover=dummy_litter_data["leaf_turnover"], - reproduct_turnover=dummy_litter_data["plant_reproductive_tissue_turnover"], - root_turnover=dummy_litter_data["root_turnover"], - metabolic_splits=metabolic_splits, - ) - - return plant_inputs - - @pytest.fixture def litter_chemistry(dummy_litter_data): """LitterChemistry object to be use throughout testing.""" @@ -189,23 +140,28 @@ def litter_chemistry(dummy_litter_data): @pytest.fixture -def input_lignin(dummy_litter_data, plant_inputs, litter_chemistry): +def input_lignin(litter_inputs): """Lignin proportion of the relevant input flows.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_lignin_concentrations, + ) - input_lignin = litter_chemistry.calculate_litter_input_lignin_concentrations( - plant_input_below_struct=plant_inputs["below_ground_structural"], - plant_input_above_struct=plant_inputs["above_ground_structural"], + input_lignin = calculate_litter_input_lignin_concentrations( + litter_inputs=litter_inputs ) return input_lignin @pytest.fixture -def input_c_n_ratios(dummy_litter_data, metabolic_splits, litter_chemistry): +def input_c_n_ratios(litter_inputs): """Carbon:nitrogen ratio of each input flow.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_nitrogen_ratios, + ) - input_c_n_ratios = litter_chemistry.calculate_litter_input_nitrogen_ratios( - metabolic_splits=metabolic_splits, + input_c_n_ratios = calculate_litter_input_nitrogen_ratios( + litter_inputs=litter_inputs, struct_to_meta_nitrogen_ratio=LitterConsts.structural_to_metabolic_n_ratio, ) @@ -213,17 +169,35 @@ def input_c_n_ratios(dummy_litter_data, metabolic_splits, litter_chemistry): @pytest.fixture -def input_c_p_ratios(dummy_litter_data, metabolic_splits, litter_chemistry): +def input_c_p_ratios(litter_inputs): """Carbon:nitrogen ratio of each input flow.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_phosphorus_ratios, + ) - input_c_p_ratios = litter_chemistry.calculate_litter_input_phosphorus_ratios( - metabolic_splits=metabolic_splits, + input_c_p_ratios = calculate_litter_input_phosphorus_ratios( + litter_inputs=litter_inputs, struct_to_meta_phosphorus_ratio=LitterConsts.structural_to_metabolic_p_ratio, ) return input_c_p_ratios +@pytest.fixture +def metabolic_splits(total_litter_input): + """Metabolic splits for the various plant inputs.""" + from virtual_ecosystem.models.litter.inputs import ( + calculate_metabolic_proportions_of_input, + ) + + metabolic_splits = calculate_metabolic_proportions_of_input( + total_input=total_litter_input, + constants=LitterConsts, + ) + + return metabolic_splits + + @pytest.fixture def post_consumption_pools(dummy_litter_data): """Pool sizes after animal consumption for each litter pool.""" @@ -254,15 +228,39 @@ def post_consumption_pools(dummy_litter_data): @pytest.fixture -def updated_pools(dummy_litter_data, decay_rates, plant_inputs, post_consumption_pools): +def total_litter_input(dummy_litter_data): + """Total input mass a chemistry for each plant biomass type.""" + from virtual_ecosystem.models.litter.inputs import combine_input_sources + + total_litter_input = combine_input_sources(dummy_litter_data) + + return total_litter_input + + +@pytest.fixture +def updated_pools( + dummy_litter_data, decay_rates, post_consumption_pools, litter_inputs +): """Updated carbon mass of each pool.""" from virtual_ecosystem.models.litter.carbon import calculate_updated_pools updated_pools = calculate_updated_pools( post_consumption_pools=post_consumption_pools, decay_rates=decay_rates, - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, update_interval=2.0, ) return updated_pools + + +@pytest.fixture +def litter_inputs(dummy_litter_data): + """Complete set of details for inputs to the litter model.""" + from virtual_ecosystem.models.litter.inputs import LitterInputs + + litter_inputs = LitterInputs.create_from_data( + data=dummy_litter_data, constants=LitterConsts + ) + + return litter_inputs diff --git a/tests/models/litter/test_carbon.py b/tests/models/litter/test_carbon.py index 415fd7248..21b93515b 100644 --- a/tests/models/litter/test_carbon.py +++ b/tests/models/litter/test_carbon.py @@ -140,14 +140,14 @@ def test_calculate_total_C_mineralised(decay_rates): def test_calculate_updated_pools( - dummy_litter_data, decay_rates, plant_inputs, post_consumption_pools + dummy_litter_data, decay_rates, post_consumption_pools, litter_inputs ): """Test that the function to calculate the pool values after the update works.""" from virtual_ecosystem.models.litter.carbon import calculate_updated_pools expected_pools = { - "above_metabolic": [0.3154561, 0.15193439, 0.07892301, 0.0712972], - "above_structural": [0.50519138, 0.25011962, 0.10250070, 0.11882651], + "above_metabolic": [0.3154788, 0.15354349, 0.080772679, 0.073701212], + "above_structural": [0.5051986807, 0.2506105228, 0.1035010262, 0.1191224962], "woody": [4.77403361, 11.89845863, 7.3598224, 7.3298224], "below_metabolic": [0.3976309, 0.3630269, 0.06787947, 0.07794085], "below_structural": [0.61050583, 0.32205947352, 0.02014514530, 0.03468376530], @@ -156,7 +156,7 @@ def test_calculate_updated_pools( actual_pools = calculate_updated_pools( post_consumption_pools=post_consumption_pools, decay_rates=decay_rates, - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, update_interval=2.0, ) diff --git a/tests/models/litter/test_chemistry.py b/tests/models/litter/test_chemistry.py index 2a0213114..c6ea58c96 100644 --- a/tests/models/litter/test_chemistry.py +++ b/tests/models/litter/test_chemistry.py @@ -27,29 +27,28 @@ def test_calculate_litter_chemistry_factor(): def test_calculate_new_pool_chemistries( - dummy_litter_data, plant_inputs, metabolic_splits, updated_pools, litter_chemistry + dummy_litter_data, litter_inputs, updated_pools, litter_chemistry ): """Test that function to calculate updated pool chemistries works correctly.""" expected_chemistries = { - "lignin_above_structural": [0.49726219, 0.10065698, 0.67693666, 0.6673972], + "lignin_above_structural": [0.49726272, 0.10113017, 0.67782882, 0.67072519], "lignin_woody": [0.49580543, 0.7978783, 0.35224272, 0.35012606], "lignin_below_structural": [0.49974338, 0.26270806, 0.74846367, 0.71955592], - "c_n_ratio_above_metabolic": [7.3918226, 8.9320212, 10.413317, 9.8624367], - "c_n_ratio_above_structural": [37.5547150, 43.3448492, 48.0974058, 52.0359678], + "c_n_ratio_above_metabolic": [7.3921805, 9.0161456, 10.4324728, 9.9183441], + "c_n_ratio_above_structural": [37.554988, 43.431768, 48.067581, 52.065169], "c_n_ratio_woody": [55.5816919, 63.2550698, 47.5208477, 59.0819914], "c_n_ratio_below_metabolic": [10.7299421, 11.3394567, 15.1984024, 12.2222413], "c_n_ratio_below_structural": [50.6228215, 55.9998994, 73.0948342, 58.6661277], - "c_p_ratio_above_metabolic": [69.965838, 68.549282, 107.38423, 96.583573], - "c_p_ratio_above_structural": [346.048307, 472.496124, 465.834123, 525.882608], + "c_p_ratio_above_metabolic": [69.966598, 69.674548, 108.426751, 96.143488], + "c_p_ratio_above_structural": [346.05231, 473.330293, 467.818240, 532.420899], "c_p_ratio_woody": [560.22870571, 762.56863636, 848.03530307, 600.40427444], "c_p_ratio_below_metabolic": [308.200782, 405.110726, 314.824814, 372.870229], "c_p_ratio_below_structural": [563.06464, 597.68324, 772.78968, 609.82810], } actual_chemistries = litter_chemistry.calculate_new_pool_chemistries( - plant_inputs=plant_inputs, - metabolic_splits=metabolic_splits, + litter_inputs=litter_inputs, updated_pools=updated_pools, ) @@ -60,19 +59,19 @@ def test_calculate_new_pool_chemistries( def test_calculate_lignin_updates( - dummy_litter_data, plant_inputs, input_lignin, updated_pools, litter_chemistry + input_lignin, updated_pools, litter_chemistry, litter_inputs ): """Test that the function to calculate the lignin updates works as expected.""" expected_lignin = { - "above_structural": [-0.00273781, 0.00065698, -0.02306334, -0.03260280], + "above_structural": [-0.0027373, 0.001130172, -0.022171178, -0.029274812], "woody": [-0.00419457, -0.0021217, 0.00224272, 0.00012606], "below_structural": [-0.00025662, 0.01270806, -0.00153633, -0.03044408], } actual_lignin = litter_chemistry.calculate_lignin_updates( input_lignin=input_lignin, - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, updated_pools=updated_pools, ) @@ -106,20 +105,20 @@ def test_calculate_change_in_chemical_concentration( def test_calculate_c_n_ratio_updates( - dummy_litter_data, plant_inputs, input_c_n_ratios, updated_pools, litter_chemistry + dummy_litter_data, litter_inputs, input_c_n_ratios, updated_pools, litter_chemistry ): """Test that calculation of C:N ratio updates works properly.""" expected_change = { - "above_metabolic": [0.091822576, 0.232021211, 0.313317200, 0.062436702], - "above_structural": [0.05471499, 0.14484922, 2.29740576, 1.835967773], + "above_metabolic": [0.0921805, 0.3161456, 0.3324728, 0.1183441], + "above_structural": [0.05498852, 0.2317676, 2.2675813, 1.8651688], "woody": [0.0816919, -0.0449302, 0.2208477, -0.0180086], "below_metabolic": [0.02994209, 0.03945672, -0.00159759, -0.17775875], "below_structural": [0.12282146, 0.39989943, -0.00516585, -2.53387232], } actual_change = litter_chemistry.calculate_c_n_ratio_updates( - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, input_c_n_ratios=input_c_n_ratios, updated_pools=updated_pools, ) @@ -131,20 +130,20 @@ def test_calculate_c_n_ratio_updates( def test_calculate_c_p_ratio_updates( - dummy_litter_data, plant_inputs, input_c_p_ratios, updated_pools, litter_chemistry + dummy_litter_data, litter_inputs, input_c_p_ratios, updated_pools, litter_chemistry ): """Test that calculation of C:P ratio updates works properly.""" expected_change = { - "above_metabolic": [12.665838, -0.15071757, 7.28423174, 0.78357281], - "above_structural": [8.5483073, -0.7038764, 50.034123, -44.317392], + "above_metabolic": [12.666598, 0.9745483, 8.3267513, 0.3434882], + "above_structural": [8.5523105, 0.13029263, 52.0182397, -37.7791012], "woody": [4.72870571, -0.73136364, 0.73530307, 1.30427444], "below_metabolic": [-2.49921796, -6.18927446, -0.37518617, -39.52977135], "below_structural": [12.56464272, 2.08324337, -0.31032454, -41.37190224], } actual_change = litter_chemistry.calculate_c_p_ratio_updates( - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, input_c_p_ratios=input_c_p_ratios, updated_pools=updated_pools, ) @@ -181,18 +180,18 @@ def test_calculate_P_mineralisation(dummy_litter_data, decay_rates, litter_chemi assert np.allclose(actual_p_mineral, expected_p_mineral) -def test_calculate_litter_input_lignin_concentrations( - dummy_litter_data, plant_inputs, litter_chemistry -): +def test_calculate_litter_input_lignin_concentrations(litter_inputs): """Check calculation of lignin concentrations of each plant flow to litter.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_lignin_concentrations, + ) expected_woody = [0.233, 0.545, 0.612, 0.378] - expected_concs_above_struct = [0.24971768, 0.22111396, 0.51122474, 0.56571041] + expected_concs_above_struct = [0.2500931, 0.2532920, 0.5303109, 0.5803457] expected_concs_below_struct = [0.48590258, 0.56412613, 0.54265483, 0.67810978] - actual_concs = litter_chemistry.calculate_litter_input_lignin_concentrations( - plant_input_below_struct=plant_inputs["below_ground_structural"], - plant_input_above_struct=plant_inputs["above_ground_structural"], + actual_concs = calculate_litter_input_lignin_concentrations( + litter_inputs=litter_inputs, ) assert np.allclose(actual_concs["woody"], expected_woody) @@ -200,21 +199,22 @@ def test_calculate_litter_input_lignin_concentrations( assert np.allclose(actual_concs["below_structural"], expected_concs_below_struct) -def test_calculate_litter_input_nitrogen_ratios( - dummy_litter_data, metabolic_splits, litter_chemistry -): +def test_calculate_litter_input_nitrogen_ratios(dummy_litter_data, litter_inputs): """Check function to calculate the C:N ratios of input to each litter pool works.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_nitrogen_ratios, + ) expected_c_n_ratios = { "woody": [60.7, 57.9, 73.1, 55.1], "below_metabolic": [11.449427, 13.09700, 14.48056, 11.04331], "below_structural": [57.24714, 65.48498, 72.40281, 55.21655], - "above_metabolic": [8.48355299, 14.17116914, 12.3424635, 11.10877484], - "above_structural": [42.5018709, 69.9028550, 64.6044513, 57.7622747], + "above_metabolic": [8.4871511, 14.7283297, 12.1855116, 11.3024309], + "above_structural": [42.52031784, 74.63602461, 63.15513757, 57.82346359], } - actual_c_n_ratios = litter_chemistry.calculate_litter_input_nitrogen_ratios( - metabolic_splits=metabolic_splits, + actual_c_n_ratios = calculate_litter_input_nitrogen_ratios( + litter_inputs=litter_inputs, struct_to_meta_nitrogen_ratio=LitterConsts.structural_to_metabolic_n_ratio, ) @@ -224,21 +224,22 @@ def test_calculate_litter_input_nitrogen_ratios( assert np.allclose(actual_c_n_ratios[key], expected_c_n_ratios[key]) -def test_calculate_litter_input_phosphorus_ratios( - dummy_litter_data, metabolic_splits, litter_chemistry -): +def test_calculate_litter_input_phosphorus_ratios(dummy_litter_data, litter_inputs): """Check function to calculate the C:P ratios of input to each litter pool works.""" + from virtual_ecosystem.models.litter.chemistry import ( + calculate_litter_input_phosphorus_ratios, + ) expected_c_p_ratios = { "woody": [856.5, 675.4, 933.2, 888.8], "below_metabolic": [248.1465, 129.418998, 146.243645, 110.700999], "below_structural": [1240.73249721, 647.09498874, 731.2182237, 553.50499377], - "above_metabolic": [220.55713162, 65.14600889, 152.23446238, 112.22496062], - "above_structural": [1118.95921, 343.440873, 825.333331, 387.658509], + "above_metabolic": [220.42737, 87.282889, 152.331456, 100.160733], + "above_structural": [1118.30505, 490.872368, 813.926271, 415.786304], } - actual_c_p_ratios = litter_chemistry.calculate_litter_input_phosphorus_ratios( - metabolic_splits=metabolic_splits, + actual_c_p_ratios = calculate_litter_input_phosphorus_ratios( + litter_inputs=litter_inputs, struct_to_meta_phosphorus_ratio=LitterConsts.structural_to_metabolic_p_ratio, ) @@ -249,7 +250,7 @@ def test_calculate_litter_input_phosphorus_ratios( def test_calculate_nutrient_split_between_litter_pools( - dummy_litter_data, metabolic_splits + dummy_litter_data, litter_inputs ): """Check the function to calculate the nutrient split between litter pools.""" from virtual_ecosystem.models.litter.chemistry import ( @@ -261,7 +262,7 @@ def test_calculate_nutrient_split_between_litter_pools( actual_meta_c_n, actual_struct_c_n = calculate_nutrient_split_between_litter_pools( input_c_nut_ratio=dummy_litter_data["root_turnover_c_n_ratio"], - metabolic_split=metabolic_splits["roots"], + metabolic_split=litter_inputs.roots_meta_split, struct_to_meta_nutrient_ratio=LitterConsts.structural_to_metabolic_n_ratio, ) @@ -278,7 +279,7 @@ def test_calculate_nutrient_split_between_litter_pools( assert np.allclose( dummy_litter_data["root_turnover_c_n_ratio"], ( - actual_meta_c_n * metabolic_splits["roots"] - + actual_struct_c_n * (1 - metabolic_splits["roots"]) + actual_meta_c_n * litter_inputs.roots_meta_split + + actual_struct_c_n * (1 - litter_inputs.roots_meta_split) ), ) diff --git a/tests/models/litter/test_input_partition.py b/tests/models/litter/test_input_partition.py deleted file mode 100644 index 8abced0d6..000000000 --- a/tests/models/litter/test_input_partition.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Test module for models.litter.input_partition.py.""" - -from logging import ERROR - -import numpy as np -import pytest - -from tests.conftest import log_check -from virtual_ecosystem.models.litter.constants import LitterConsts - - -def test_calculate_metabolic_proportions_of_input(dummy_litter_data): - """Test that function to calculate metabolic input proportions works as expected.""" - - from virtual_ecosystem.models.litter.input_partition import ( - calculate_metabolic_proportions_of_input, - ) - - expected_proportions = { - "leaves": [0.812403025, 0.640197595, 0.424077745, 0.0089426731], - "reproductive": [0.8462925685, 0.833489905, 0.83196046, 0.8390536408], - "roots": [0.588394858, 0.379571377, 0.5024461477, 0.410125012], - } - - actual_proportions = calculate_metabolic_proportions_of_input( - leaf_turnover_lignin_proportion=dummy_litter_data["leaf_turnover_lignin"], - reproduct_turnover_lignin_proportion=dummy_litter_data[ - "plant_reproductive_tissue_turnover_lignin" - ], - root_turnover_lignin_proportion=dummy_litter_data["root_turnover_lignin"], - leaf_turnover_c_n_ratio=dummy_litter_data["leaf_turnover_c_n_ratio"], - reproduct_turnover_c_n_ratio=dummy_litter_data[ - "plant_reproductive_tissue_turnover_c_n_ratio" - ], - root_turnover_c_n_ratio=dummy_litter_data["root_turnover_c_n_ratio"], - leaf_turnover_c_p_ratio=dummy_litter_data["leaf_turnover_c_p_ratio"], - reproduct_turnover_c_p_ratio=dummy_litter_data[ - "plant_reproductive_tissue_turnover_c_p_ratio" - ], - root_turnover_c_p_ratio=dummy_litter_data["root_turnover_c_p_ratio"], - constants=LitterConsts, - ) - - assert set(expected_proportions.keys()) == set(actual_proportions.keys()) - - for key in actual_proportions.keys(): - assert np.allclose(actual_proportions[key], expected_proportions[key]) - - -def test_partion_plant_inputs_between_pools(dummy_litter_data, metabolic_splits): - """Check function to partition inputs into litter pools works as expected.""" - - from virtual_ecosystem.models.litter.input_partition import ( - partion_plant_inputs_between_pools, - ) - - expected_woody = [0.075, 0.099, 0.063, 0.033] - expected_above_meta = [0.02447376, 0.00644323, 0.01102713, 0.00340132] - expected_above_struct = [0.00552624, 0.00135677, 0.01252287, 0.02884868] - expected_below_meta = [0.01588666, 0.00797100, 0.00015073, 0.01021211] - expected_below_struct = [0.01111334, 0.013029, 0.00014927, 0.01468789] - - actual_splits = partion_plant_inputs_between_pools( - deadwood_production=dummy_litter_data["deadwood_production"], - leaf_turnover=dummy_litter_data["leaf_turnover"], - reproduct_turnover=dummy_litter_data["plant_reproductive_tissue_turnover"], - root_turnover=dummy_litter_data["root_turnover"], - metabolic_splits=metabolic_splits, - ) - - assert np.allclose(actual_splits["woody"], expected_woody) - assert np.allclose(actual_splits["above_ground_metabolic"], expected_above_meta) - assert np.allclose(actual_splits["above_ground_structural"], expected_above_struct) - assert np.allclose(actual_splits["below_ground_metabolic"], expected_below_meta) - assert np.allclose(actual_splits["below_ground_structural"], expected_below_struct) - - -def test_split_pool_into_metabolic_and_structural_litter(dummy_litter_data): - """Check function to split input biomass between litter pools works as expected.""" - - from virtual_ecosystem.models.litter.input_partition import ( - split_pool_into_metabolic_and_structural_litter, - ) - - expected_split = [0.812403025, 0.640197595, 0.424077745, 0.0089426731] - - actual_split = split_pool_into_metabolic_and_structural_litter( - lignin_proportion=dummy_litter_data["leaf_turnover_lignin"], - carbon_nitrogen_ratio=dummy_litter_data["leaf_turnover_c_n_ratio"], - carbon_phosphorus_ratio=dummy_litter_data["leaf_turnover_c_p_ratio"], - max_metabolic_fraction=LitterConsts.max_metabolic_fraction_of_input, - split_sensitivity_nitrogen=LitterConsts.metabolic_split_nitrogen_sensitivity, - split_sensitivity_phosphorus=LitterConsts.metabolic_split_phosphorus_sensitivity, - ) - - assert np.allclose(actual_split, expected_split) - - -@pytest.mark.parametrize( - "c_n_ratios,expected_log", - [ - pytest.param( - np.array([34.2, 55.5, 37.1, 400.7]), - ( - ( - ERROR, - "Fraction of input biomass going to metabolic pool has dropped " - "below zero!", - ), - ), - id="negative_metabolic_flow", - ), - pytest.param( - np.array([34.2, 55.5, 37.1, 3.7]), - ( - ( - ERROR, - "Fraction of input biomass going to structural biomass is less than" - " the lignin fraction!", - ), - ), - id="less_than_lignin", - ), - ], -) -def test_split_pool_into_metabolic_and_structural_litter_bad_data( - caplog, c_n_ratios, expected_log -): - """Check that pool split functions raises an error if out of bounds data is used.""" - - from virtual_ecosystem.models.litter.input_partition import ( - split_pool_into_metabolic_and_structural_litter, - ) - - # C:N ratio of >400 is far too high for the function to behave sensibly - lignin_proportions = np.array([0.5, 0.4, 0.35, 0.23]) - c_p_ratios = np.array([[415.0, 327.4, 554.5, 145.0]]) - - with pytest.raises(ValueError): - split_pool_into_metabolic_and_structural_litter( - lignin_proportion=lignin_proportions, - carbon_nitrogen_ratio=c_n_ratios, - carbon_phosphorus_ratio=c_p_ratios, - max_metabolic_fraction=LitterConsts.max_metabolic_fraction_of_input, - split_sensitivity_nitrogen=LitterConsts.metabolic_split_nitrogen_sensitivity, - split_sensitivity_phosphorus=LitterConsts.metabolic_split_phosphorus_sensitivity, - ) - - # Check the error reports - log_check(caplog, expected_log) diff --git a/tests/models/litter/test_inputs.py b/tests/models/litter/test_inputs.py new file mode 100644 index 000000000..627eb290c --- /dev/null +++ b/tests/models/litter/test_inputs.py @@ -0,0 +1,210 @@ +"""Test module for models.litter.inputs.py.""" + +from logging import ERROR + +import numpy as np +import pytest + +from tests.conftest import log_check +from virtual_ecosystem.models.litter.constants import LitterConsts + + +def test_determine_all_plant_to_litter_flows(dummy_litter_data): + """Test that function to determine plant to litter flows works correctly.""" + from dataclasses import asdict + + from virtual_ecosystem.models.litter.inputs import LitterInputs + + expected_inputs = { + "leaves_meta_split": [0.8123412282, 0.7504823457, 0.4509559749, 0.0852205423], + "reproduct_meta_split": [0.8462925685, 0.833489905, 0.83196046, 0.8390536408], + "roots_meta_split": [0.588394858, 0.379571377, 0.5024461477, 0.410125012], + "input_woody": [0.075, 0.099, 0.063, 0.033], + "input_above_metabolic": [0.02449646, 0.00805233, 0.0128768, 0.00580533], + "input_above_structural": [0.00553354, 0.00184767, 0.0135232, 0.0291447], + "input_below_metabolic": [0.01588666, 0.007971, 0.00015073, 0.01021211], + "input_below_structural": [0.01111334, 0.013029, 0.00014927, 0.01468789], + "leaf_mass": [0.02703, 0.0024, 0.02385, 0.0312], + "root_mass": [0.027, 0.021, 0.0003, 0.0249], + "deadwood_mass": [0.075, 0.099, 0.063, 0.033], + "reprod_mass": [0.003, 0.0075, 0.00255, 0.00375], + "leaf_lignin": [0.05008879, 0.10125, 0.29641509, 0.53971154], + "root_lignin": [0.2, 0.35, 0.27, 0.4], + "deadwood_lignin": [0.233, 0.545, 0.612, 0.378], + "reprod_lignin": [0.01, 0.03, 0.04, 0.02], + "leaf_nitrogen": [15.00899, 32.5, 40.710063, 53.929808], + "root_nitrogen": [30.3, 45.6, 43.3, 37.1], + "deadwood_nitrogen": [60.7, 57.9, 73.1, 55.1], + "reprod_nitrogen": [12.5, 23.8, 15.7, 18.2], + "leaf_phosphorus": [414.77525, 342.625, 528.24654, 384.29231], + "root_phosphorus": [656.7, 450.6, 437.3, 371.9], + "deadwood_phosphorus": [856.5, 675.4, 933.2, 888.8], + "reprod_phosphorus": [125.5, 105.0, 145.0, 189.2], + } + + litter_inputs = LitterInputs.create_from_data( + data=dummy_litter_data, constants=LitterConsts + ) + # Check that the right sort of object has been created + assert isinstance(litter_inputs, LitterInputs) + + # Then convert to a dict to check the values + litter_inputs = asdict(litter_inputs) + + # Check that all keys match and have correct values for both dictionaries + assert set(expected_inputs.keys()) == set(litter_inputs.keys()) + + for key in litter_inputs.keys(): + assert np.allclose(litter_inputs[key], expected_inputs[key]) + + +def test_combine_input_sources(dummy_litter_data): + """Test that function to combine input sources works as expected.""" + from virtual_ecosystem.models.litter.inputs import combine_input_sources + + expected_combined = { + "leaf_mass": [0.02703, 0.0024, 0.02385, 0.0312], + "root_mass": [0.027, 0.021, 0.0003, 0.0249], + "deadwood_mass": [0.075, 0.099, 0.063, 0.033], + "reprod_mass": [0.003, 0.0075, 0.00255, 0.00375], + "leaf_lignin": [0.05008879, 0.10125, 0.29641509, 0.53971154], + "root_lignin": [0.2, 0.35, 0.27, 0.4], + "deadwood_lignin": [0.233, 0.545, 0.612, 0.378], + "reprod_lignin": [0.01, 0.03, 0.04, 0.02], + "leaf_nitrogen": [15.00899, 32.5, 40.710063, 53.929808], + "root_nitrogen": [30.3, 45.6, 43.3, 37.1], + "deadwood_nitrogen": [60.7, 57.9, 73.1, 55.1], + "reprod_nitrogen": [12.5, 23.8, 15.7, 18.2], + "leaf_phosphorus": [414.77525, 342.625, 528.24654, 384.29231], + "root_phosphorus": [656.7, 450.6, 437.3, 371.9], + "deadwood_phosphorus": [856.5, 675.4, 933.2, 888.8], + "reprod_phosphorus": [125.5, 105.0, 145.0, 189.2], + } + + actual_combined = combine_input_sources(dummy_litter_data) + + assert set(expected_combined.keys()) == set(actual_combined.keys()) + + for key in actual_combined.keys(): + assert np.allclose(actual_combined[key], expected_combined[key]) + + +def test_calculate_metabolic_proportions_of_input(total_litter_input): + """Test that function to calculate metabolic input proportions works as expected.""" + from virtual_ecosystem.models.litter.inputs import ( + calculate_metabolic_proportions_of_input, + ) + + expected_proportions = { + "leaves_meta_split": [0.8123412282, 0.7504823457, 0.4509559749, 0.0852205423], + "reproduct_meta_split": [0.8462925685, 0.833489905, 0.83196046, 0.8390536408], + "roots_meta_split": [0.588394858, 0.379571377, 0.5024461477, 0.410125012], + } + + actual_proportions = calculate_metabolic_proportions_of_input( + total_input=total_litter_input, constants=LitterConsts + ) + + assert set(expected_proportions.keys()) == set(actual_proportions.keys()) + + for key in actual_proportions.keys(): + assert np.allclose(actual_proportions[key], expected_proportions[key]) + + +def test_partion_plant_inputs_between_pools(metabolic_splits, total_litter_input): + """Check function to partition inputs into litter pools works as expected.""" + from virtual_ecosystem.models.litter.inputs import ( + partion_plant_inputs_between_pools, + ) + + expected_inputs = { + "input_woody": [0.075, 0.099, 0.063, 0.033], + "input_above_metabolic": [0.02449646, 0.00805233, 0.0128768, 0.00580533], + "input_above_structural": [0.00553354, 0.00184767, 0.0135232, 0.02914467], + "input_below_metabolic": [0.01588666, 0.007971, 0.00015073, 0.01021211], + "input_below_structural": [0.01111334, 0.013029, 0.00014927, 0.01468789], + } + + actual_inputs = partion_plant_inputs_between_pools( + total_input=total_litter_input, metabolic_splits=metabolic_splits + ) + + assert set(expected_inputs.keys()) == set(actual_inputs.keys()) + + for key in actual_inputs.keys(): + assert np.allclose(actual_inputs[key], expected_inputs[key]) + + +def test_split_pool_into_metabolic_and_structural_litter(dummy_litter_data): + """Check function to split input biomass between litter pools works as expected.""" + + from virtual_ecosystem.models.litter.inputs import ( + split_pool_into_metabolic_and_structural_litter, + ) + + expected_split = [0.812403025, 0.640197595, 0.424077745, 0.0089426731] + + actual_split = split_pool_into_metabolic_and_structural_litter( + lignin_proportion=dummy_litter_data["leaf_turnover_lignin"], + carbon_nitrogen_ratio=dummy_litter_data["leaf_turnover_c_n_ratio"], + carbon_phosphorus_ratio=dummy_litter_data["leaf_turnover_c_p_ratio"], + max_metabolic_fraction=LitterConsts.max_metabolic_fraction_of_input, + split_sensitivity_nitrogen=LitterConsts.metabolic_split_nitrogen_sensitivity, + split_sensitivity_phosphorus=LitterConsts.metabolic_split_phosphorus_sensitivity, + ) + + assert np.allclose(actual_split, expected_split) + + +@pytest.mark.parametrize( + "c_n_ratios,expected_log", + [ + pytest.param( + np.array([34.2, 55.5, 37.1, 400.7]), + ( + ( + ERROR, + "Fraction of input biomass going to metabolic pool has dropped " + "below zero!", + ), + ), + id="negative_metabolic_flow", + ), + pytest.param( + np.array([34.2, 55.5, 37.1, 3.7]), + ( + ( + ERROR, + "Fraction of input biomass going to structural biomass is less than" + " the lignin fraction!", + ), + ), + id="less_than_lignin", + ), + ], +) +def test_split_pool_into_metabolic_and_structural_litter_bad_data( + caplog, c_n_ratios, expected_log +): + """Check that pool split functions raises an error if out of bounds data is used.""" + + from virtual_ecosystem.models.litter.inputs import ( + split_pool_into_metabolic_and_structural_litter, + ) + + # C:N ratio of >400 is far too high for the function to behave sensibly + lignin_proportions = np.array([0.5, 0.4, 0.35, 0.23]) + c_p_ratios = np.array([[415.0, 327.4, 554.5, 145.0]]) + + with pytest.raises(ValueError): + split_pool_into_metabolic_and_structural_litter( + lignin_proportion=lignin_proportions, + carbon_nitrogen_ratio=c_n_ratios, + carbon_phosphorus_ratio=c_p_ratios, + max_metabolic_fraction=LitterConsts.max_metabolic_fraction_of_input, + split_sensitivity_nitrogen=LitterConsts.metabolic_split_nitrogen_sensitivity, + split_sensitivity_phosphorus=LitterConsts.metabolic_split_phosphorus_sensitivity, + ) + + # Check the error reports + log_check(caplog, expected_log) diff --git a/tests/models/litter/test_litter_model.py b/tests/models/litter/test_litter_model.py index bcc878580..8ba88dbd6 100644 --- a/tests/models/litter/test_litter_model.py +++ b/tests/models/litter/test_litter_model.py @@ -457,21 +457,21 @@ def test_generate_litter_model( def test_update(fixture_litter_model, dummy_litter_data): """Test to check that the update step works and increments the update step.""" - end_above_meta = [0.3154561, 0.15193439, 0.07892301, 0.0712972] - end_above_struct = [0.50519138, 0.25011962, 0.10250070, 0.11882651] + end_above_meta = [0.3154788, 0.15354349, 0.080772679, 0.073701212] + end_above_struct = [0.5051986807, 0.2506105228, 0.1035010262, 0.1191224962] end_woody = [4.77403361, 11.89845863, 7.3598224, 7.3298224] end_below_meta = [0.3976309, 0.3630269, 0.06787947, 0.07794085] end_below_struct = [0.61050583, 0.32205947352, 0.02014514530, 0.03468376530] - end_lignin_above_struct = [0.49726219, 0.10065698, 0.67693666, 0.6673972] + end_lignin_above_struct = [0.49726272, 0.10113017, 0.67782882, 0.67072519] end_lignin_woody = [0.49580543, 0.7978783, 0.35224272, 0.35012606] end_lignin_below_struct = [0.49974338, 0.26270806, 0.74846367, 0.71955592] - end_c_n_above_metabolic = [7.3918226, 8.9320212, 10.413317, 9.8624367] - end_c_n_above_structural = [37.5547150, 43.3448492, 48.0974058, 52.0359678] + end_c_n_above_metabolic = [7.3921805, 9.0161456, 10.4324728, 9.9183441] + end_c_n_above_structural = [37.554988, 43.431768, 48.067581, 52.065169] end_c_n_woody = [55.5816919, 63.2550698, 47.5208477, 59.0819914] end_c_n_below_metabolic = [10.7299421, 11.3394567, 15.1984024, 12.2222413] end_c_n_below_structural = [50.6228215, 55.9998994, 73.0948342, 58.6661277] - end_c_p_above_metabolic = [69.965838, 68.549282, 107.38423, 96.583573] - end_c_p_above_structural = [346.048307, 472.496124, 465.834123, 525.882608] + end_c_p_above_metabolic = [69.966598, 69.674548, 108.426751, 96.143488] + end_c_p_above_structural = [346.05231, 473.330293, 467.818240, 532.420899] end_c_p_woody = [560.22870571, 762.56863636, 848.03530307, 600.40427444] end_c_p_below_metabolic = [308.200782, 405.110726, 314.824814, 372.870229] end_c_p_below_structural = [563.06464, 597.68324, 772.78968, 609.82810] diff --git a/virtual_ecosystem/data_variables.toml b/virtual_ecosystem/data_variables.toml index 65c0cd9ad..d5ab69b32 100644 --- a/virtual_ecosystem/data_variables.toml +++ b/virtual_ecosystem/data_variables.toml @@ -775,6 +775,34 @@ name = "decomposed_carcasses_phosphorus" unit = "kg P m^-3 day^-1" variable_type = "float" +[[variable]] +axis = ["spatial"] +description = "Mass of leaves added to the litter due to herbivore mechanical inefficiency" +name = "herbivory_waste_leaf_carbon" +unit = "kg C m^-2" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Carbon nitrogen ratio of leaves added to litter due to herbivore mechanical inefficiency" +name = "herbivory_waste_leaf_nitrogen" +unit = "-" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Carbon phosphorus ratio of leaves added to litter due to herbivore mechanical inefficiency" +name = "herbivory_waste_leaf_phosphorus" +unit = "-" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Lignin proportion of leaves added to litter due to herbivore mechanical inefficiency" +name = "herbivory_waste_leaf_lignin" +unit = "-" +variable_type = "float" + [[variable]] axis = ["spatial"] description = "Amount of above-ground metabolic litter that has been consumed by animals" diff --git a/virtual_ecosystem/models/animal/animal_model.py b/virtual_ecosystem/models/animal/animal_model.py index 3f86eb9cd..8461f98ff 100644 --- a/virtual_ecosystem/models/animal/animal_model.py +++ b/virtual_ecosystem/models/animal/animal_model.py @@ -67,6 +67,10 @@ class AnimalModel( "decomposed_carcasses_carbon", "decomposed_carcasses_nitrogen", "decomposed_carcasses_phosphorus", + "herbivory_waste_leaf_carbon", + "herbivory_waste_leaf_nitrogen", + "herbivory_waste_leaf_phosphorus", + "herbivory_waste_leaf_lignin", "litter_consumption_above_metabolic", "litter_consumption_above_structural", "litter_consumption_woody", @@ -80,6 +84,10 @@ class AnimalModel( "decomposed_carcasses_carbon", "decomposed_carcasses_nitrogen", "decomposed_carcasses_phosphorus", + "herbivory_waste_leaf_carbon", + "herbivory_waste_leaf_nitrogen", + "herbivory_waste_leaf_phosphorus", + "herbivory_waste_leaf_lignin", "total_animal_respiration", "litter_consumption_above_metabolic", "litter_consumption_above_structural", @@ -303,11 +311,12 @@ def update(self, time_index: int, **kwargs: Any) -> None: # soil and litter models can be extracted additions_to_soil = self.calculate_soil_additions() litter_consumption = self.calculate_total_litter_consumption(litter_pools) - # TODO - Actually do something with this - _ = self.calculate_litter_additions_from_herbivory() + litter_additions = self.calculate_litter_additions_from_herbivory() # Update the data object with the changes to soil and litter pools - self.data.add_from_dict(additions_to_soil | litter_consumption) + self.data.add_from_dict( + additions_to_soil | litter_consumption | litter_additions + ) # TODO - TEST THIS! # Update population densities self.update_population_densities() diff --git a/virtual_ecosystem/models/litter/__init__.py b/virtual_ecosystem/models/litter/__init__.py index 10fb8710c..d0885c29c 100644 --- a/virtual_ecosystem/models/litter/__init__.py +++ b/virtual_ecosystem/models/litter/__init__.py @@ -11,8 +11,8 @@ pools that the litter model is comprised of. * :mod:`~virtual_ecosystem.models.litter.chemistry` tracks the chemistry (lignin, nitrogen and phosphorus) of the litter pools. -* :mod:`~virtual_ecosystem.models.litter.input_partition` handles the partitioning - of biomass input between the different litter pools. +* :mod:`~virtual_ecosystem.models.litter.inputs` handles the partitioning of biomass + input between the different litter pools. * :mod:`~virtual_ecosystem.models.litter.env_factors` provides the functions capturing the impact of environmental factors on litter decay. * :mod:`~virtual_ecosystem.models.litter.constants` provides a set of dataclasses diff --git a/virtual_ecosystem/models/litter/carbon.py b/virtual_ecosystem/models/litter/carbon.py index a968dbdbf..f4b8383d5 100644 --- a/virtual_ecosystem/models/litter/carbon.py +++ b/virtual_ecosystem/models/litter/carbon.py @@ -24,6 +24,7 @@ from virtual_ecosystem.models.litter.env_factors import ( calculate_environmental_factors, ) +from virtual_ecosystem.models.litter.inputs import LitterInputs def calculate_post_consumption_pools( @@ -218,7 +219,7 @@ def calculate_total_C_mineralised( def calculate_updated_pools( post_consumption_pools: dict[str, NDArray[np.float32]], decay_rates: dict[str, NDArray[np.float32]], - plant_inputs: dict[str, NDArray[np.float32]], + litter_inputs: LitterInputs, update_interval: float, ) -> dict[str, NDArray[np.float32]]: """Calculate the updated mass of each litter pool. @@ -231,8 +232,10 @@ def calculate_updated_pools( subtracted [kg C m^-2] decay_rates: Dictionary containing the rates of decay for all 5 litter pools [kg C m^-2 day^-1] - plant_inputs: Dictionary containing the amount of each litter type that is added - from the plant model in this time step [kg C m^-2] + 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 @@ -244,17 +247,19 @@ def calculate_updated_pools( # Net pool changes are found by combining input and decay rates, and then # multiplying by the update time step. - change_in_metabolic_above = plant_inputs["above_ground_metabolic"] - ( + change_in_metabolic_above = litter_inputs.input_above_metabolic - ( decay_rates["metabolic_above"] * update_interval ) - change_in_structural_above = plant_inputs["above_ground_structural"] - ( + change_in_structural_above = litter_inputs.input_above_structural - ( decay_rates["structural_above"] * update_interval ) - change_in_woody = plant_inputs["woody"] - (decay_rates["woody"] * update_interval) - change_in_metabolic_below = plant_inputs["below_ground_metabolic"] - ( + change_in_woody = litter_inputs.input_woody - ( + decay_rates["woody"] * update_interval + ) + change_in_metabolic_below = litter_inputs.input_below_metabolic - ( decay_rates["metabolic_below"] * update_interval ) - change_in_structural_below = plant_inputs["below_ground_structural"] - ( + change_in_structural_below = litter_inputs.input_below_structural - ( decay_rates["structural_below"] * update_interval ) diff --git a/virtual_ecosystem/models/litter/chemistry.py b/virtual_ecosystem/models/litter/chemistry.py index 590c7d9c5..37e6532e5 100644 --- a/virtual_ecosystem/models/litter/chemistry.py +++ b/virtual_ecosystem/models/litter/chemistry.py @@ -8,7 +8,7 @@ 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.input_partition`), which indirectly captures the +: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 @@ -19,6 +19,7 @@ from virtual_ecosystem.core.data import Data from virtual_ecosystem.models.litter.constants import LitterConsts +from virtual_ecosystem.models.litter.inputs import LitterInputs class LitterChemistry: @@ -36,9 +37,8 @@ def __init__(self, data: Data, constants: LitterConsts): def calculate_new_pool_chemistries( self, - plant_inputs: dict[str, NDArray[np.float32]], - metabolic_splits: dict[str, NDArray[np.float32]], updated_pools: dict[str, NDArray[np.float32]], + litter_inputs: LitterInputs, ) -> dict[str, DataArray]: """Method to calculate the updated chemistry of each litter pool. @@ -47,42 +47,40 @@ def calculate_new_pool_chemistries( lignin, so it is only updated for those pools. Args: - plant_inputs: Dictionary containing the amount of each litter type that is - added from the plant model in this time step [kg C m^-2] - 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] updated_pools: Dictionary containing the updated pool densities for all 5 litter pools [kg C m^-2] + 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. """ # Find lignin and nitrogen contents of the litter input flows - input_lignin = self.calculate_litter_input_lignin_concentrations( - plant_input_above_struct=plant_inputs["above_ground_structural"], - plant_input_below_struct=plant_inputs["below_ground_structural"], + input_lignin = calculate_litter_input_lignin_concentrations( + litter_inputs=litter_inputs, ) - input_c_n_ratios = self.calculate_litter_input_nitrogen_ratios( - metabolic_splits=metabolic_splits, + input_c_n_ratios = calculate_litter_input_nitrogen_ratios( + litter_inputs=litter_inputs, struct_to_meta_nitrogen_ratio=self.structural_to_metabolic_n_ratio, ) - input_c_p_ratios = self.calculate_litter_input_phosphorus_ratios( - metabolic_splits=metabolic_splits, + input_c_p_ratios = calculate_litter_input_phosphorus_ratios( + litter_inputs=litter_inputs, struct_to_meta_phosphorus_ratio=self.structural_to_metabolic_p_ratio, ) # Then use to find the changes change_in_lignin = self.calculate_lignin_updates( - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, input_lignin=input_lignin, updated_pools=updated_pools, ) change_in_c_n_ratios = self.calculate_c_n_ratio_updates( - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, input_c_n_ratios=input_c_n_ratios, updated_pools=updated_pools, ) change_in_c_p_ratios = self.calculate_c_p_ratio_updates( - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, input_c_p_ratios=input_c_p_ratios, updated_pools=updated_pools, ) @@ -121,266 +119,9 @@ def calculate_new_pool_chemistries( return lignin_changes | nitrogen_changes | phosphorus_changes - def calculate_litter_input_lignin_concentrations( - self, - plant_input_below_struct: NDArray[np.float32], - plant_input_above_struct: NDArray[np.float32], - ) -> dict[str, NDArray[np.float32]]: - """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 with the combined - total lignin content of the leaf and reproductive matter inputs being found, and - then converted to a back into a concentration. - - Args: - plant_input_below_struct: Plant input to below ground structural litter pool - [kg C m^-2] - plant_input_above_struct: Plant input to above ground structural litter pool - [kg C m^-2] - - 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 kg C^-1] - """ - - lignin_proportion_woody = self.data["deadwood_lignin"] - - lignin_proportion_below_structural = ( - self.data["root_turnover_lignin"] - * self.data["root_turnover"] - / plant_input_below_struct - ) - - lignin_proportion_above_structural = ( - (self.data["leaf_turnover_lignin"] * self.data["leaf_turnover"]) - + ( - self.data["plant_reproductive_tissue_turnover_lignin"] - * self.data["plant_reproductive_tissue_turnover"] - ) - ) / plant_input_above_struct - - return { - "woody": lignin_proportion_woody.to_numpy(), - "below_structural": lignin_proportion_below_structural.to_numpy(), - "above_structural": lignin_proportion_above_structural.to_numpy(), - } - - def calculate_litter_input_nitrogen_ratios( - self, - metabolic_splits: dict[str, NDArray[np.float32]], - struct_to_meta_nitrogen_ratio: float, - ) -> dict[str, NDArray[np.float32]]: - """Calculate the carbon to nitrogen ratio for each plant biomass to litter flow. - - The ratio for the input to the woody litter pool just matches the ratio of the - deadwood input. For the below ground pools, the ratios of the flows from root - turnover into the metabolic and structural pools is calculated. A similar - approach is taken for the above ground metabolic and structural pools, but here - a weighted average of the two contributions to each pool (leaf and reproductive - tissue turnover) must be taken. - - Args: - 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] - struct_to_meta_nitrogen_ratio: Ratio of the carbon to nitrogen ratios of - structural vs metabolic litter pools [unitless] - - Returns: - Dictionary containing the carbon to nitrogen ratios of the input to each of - the pools [unitless] - """ - - # Calculate c_n_ratio split for each (non-wood) input biomass type - root_c_n_ratio_meta, root_c_n_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data["root_turnover_c_n_ratio"].to_numpy(), - metabolic_split=metabolic_splits["roots"], - struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, - ) - ) - - leaf_c_n_ratio_meta, leaf_c_n_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data["leaf_turnover_c_n_ratio"].to_numpy(), - metabolic_split=metabolic_splits["leaves"], - struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, - ) - ) - - reprod_c_n_ratio_meta, reprod_c_n_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data[ - "plant_reproductive_tissue_turnover_c_n_ratio" - ].to_numpy(), - metabolic_split=metabolic_splits["reproductive"], - struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, - ) - ) - - c_n_ratio_below_metabolic = root_c_n_ratio_meta - c_n_ratio_below_structural = root_c_n_ratio_struct - c_n_ratio_woody = self.data["deadwood_c_n_ratio"].to_numpy() - # Inputs with multiple sources have to be weighted - c_n_ratio_above_metabolic = np.divide( - ( - leaf_c_n_ratio_meta - * self.data["leaf_turnover"] - * metabolic_splits["leaves"] - ) - + ( - reprod_c_n_ratio_meta - * self.data["plant_reproductive_tissue_turnover"] - * metabolic_splits["reproductive"] - ), - (self.data["leaf_turnover"] * metabolic_splits["leaves"]) - + ( - self.data["plant_reproductive_tissue_turnover"] - * metabolic_splits["reproductive"] - ), - ) - - c_n_ratio_above_structural = np.divide( - ( - leaf_c_n_ratio_struct - * self.data["leaf_turnover"] - * (1 - metabolic_splits["leaves"]) - ) - + ( - reprod_c_n_ratio_struct - * self.data["plant_reproductive_tissue_turnover"] - * (1 - metabolic_splits["reproductive"]) - ), - (self.data["leaf_turnover"] * (1 - metabolic_splits["leaves"])) - + ( - self.data["plant_reproductive_tissue_turnover"] - * (1 - metabolic_splits["reproductive"]) - ), - ) - - return { - "woody": c_n_ratio_woody, - "below_metabolic": c_n_ratio_below_metabolic, - "below_structural": c_n_ratio_below_structural, - "above_metabolic": c_n_ratio_above_metabolic, - "above_structural": c_n_ratio_above_structural, - } - - def calculate_litter_input_phosphorus_ratios( - self, - metabolic_splits: dict[str, NDArray[np.float32]], - struct_to_meta_phosphorus_ratio: float, - ) -> dict[str, NDArray[np.float32]]: - """Calculate carbon to phosphorus ratio for each plant biomass to litter flow. - - The ratio for the input to the woody litter pool just matches the ratio of the - deadwood input. For the below ground pools, the ratios of the flows from root - turnover into the metabolic and structural pools is calculated. A similar - approach is taken for the above ground metabolic and structural pools, but here - a weighted average of the two contributions to each pool (leaf and reproductive - tissue turnover) must be taken. - - Args: - 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] - struct_to_meta_phosphorus_ratio: Ratio of the carbon to phosphorus ratios of - structural vs metabolic litter pools [unitless] - - Returns: - Dictionary containing the carbon to phosphorus ratios of the input to each - of the pools [unitless] - """ - - # Calculate c_p_ratio split for each (non-wood) input biomass type - root_c_p_ratio_meta, root_c_p_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data["root_turnover_c_p_ratio"].to_numpy(), - metabolic_split=metabolic_splits["roots"], - struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, - ) - ) - - leaf_c_p_ratio_meta, leaf_c_p_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data["leaf_turnover_c_p_ratio"].to_numpy(), - metabolic_split=metabolic_splits["leaves"], - struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, - ) - ) - - reprod_c_p_ratio_meta, reprod_c_p_ratio_struct = ( - calculate_nutrient_split_between_litter_pools( - input_c_nut_ratio=self.data[ - "plant_reproductive_tissue_turnover_c_p_ratio" - ].to_numpy(), - metabolic_split=metabolic_splits["reproductive"], - struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, - ) - ) - - c_p_ratio_below_metabolic = root_c_p_ratio_meta - c_p_ratio_below_structural = root_c_p_ratio_struct - c_p_ratio_woody = self.data["deadwood_c_p_ratio"].to_numpy() - # Inputs with multiple sources have to be weighted - c_p_ratio_above_metabolic = np.divide( - ( - leaf_c_p_ratio_meta - * self.data["leaf_turnover"] - * metabolic_splits["leaves"] - ) - + ( - reprod_c_p_ratio_meta - * self.data["plant_reproductive_tissue_turnover"] - * metabolic_splits["reproductive"] - ), - (self.data["leaf_turnover"] * metabolic_splits["leaves"]) - + ( - self.data["plant_reproductive_tissue_turnover"] - * metabolic_splits["reproductive"] - ), - ) - - c_p_ratio_above_structural = np.divide( - ( - leaf_c_p_ratio_struct - * self.data["leaf_turnover"] - * (1 - metabolic_splits["leaves"]) - ) - + ( - reprod_c_p_ratio_struct - * self.data["plant_reproductive_tissue_turnover"] - * (1 - metabolic_splits["reproductive"]) - ), - (self.data["leaf_turnover"] * (1 - metabolic_splits["leaves"])) - + ( - self.data["plant_reproductive_tissue_turnover"] - * (1 - metabolic_splits["reproductive"]) - ), - ) - - return { - "woody": c_p_ratio_woody, - "below_metabolic": c_p_ratio_below_metabolic, - "below_structural": c_p_ratio_below_structural, - "above_metabolic": c_p_ratio_above_metabolic, - "above_structural": c_p_ratio_above_structural, - } - def calculate_lignin_updates( self, - plant_inputs: dict[str, NDArray[np.float32]], + litter_inputs: LitterInputs, input_lignin: dict[str, NDArray[np.float32]], updated_pools: dict[str, NDArray[np.float32]], ) -> dict[str, NDArray[np.float32]]: @@ -391,8 +132,10 @@ def calculate_lignin_updates( used in an integration process. Args: - plant_inputs: Dictionary containing the amount of each litter type that is - added from the plant model in this time step [kg C m^-2] + 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_lignin: Dictionary containing the lignin concentration of the input to each of the three lignin containing litter pools [kg lignin kg C^-1] updated_pools: Dictionary containing the updated pool densities for all 5 @@ -405,19 +148,19 @@ def calculate_lignin_updates( """ change_in_lignin_above_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["above_ground_structural"], + input_carbon=litter_inputs.input_above_structural, updated_pool_carbon=updated_pools["above_structural"], input_conc=input_lignin["above_structural"], old_pool_conc=self.data["lignin_above_structural"].to_numpy(), ) change_in_lignin_woody = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["woody"], + input_carbon=litter_inputs.input_woody, updated_pool_carbon=updated_pools["woody"], input_conc=input_lignin["woody"], old_pool_conc=self.data["lignin_woody"].to_numpy(), ) change_in_lignin_below_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["below_ground_structural"], + input_carbon=litter_inputs.input_below_structural, updated_pool_carbon=updated_pools["below_structural"], input_conc=input_lignin["below_structural"], old_pool_conc=self.data["lignin_below_structural"].to_numpy(), @@ -431,7 +174,7 @@ def calculate_lignin_updates( def calculate_c_n_ratio_updates( self, - plant_inputs: dict[str, NDArray[np.float32]], + litter_inputs: LitterInputs, input_c_n_ratios: dict[str, NDArray[np.float32]], updated_pools: dict[str, NDArray[np.float32]], ) -> dict[str, NDArray[np.float32]]: @@ -441,8 +184,10 @@ def calculate_c_n_ratio_updates( be used in an integration process. Args: - plant_inputs: Dictionary containing the amount of each litter type that is - added from the plant model in this time step [kg C m^-2] + 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_c_n_ratios: Dictionary containing the carbon to nitrogen ratios of the input to each of the litter pools [unitless] updated_pools: Dictionary containing the updated pool densities for all 5 @@ -454,31 +199,31 @@ def calculate_c_n_ratio_updates( """ change_in_n_above_metabolic = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["above_ground_metabolic"], + input_carbon=litter_inputs.input_above_metabolic, updated_pool_carbon=updated_pools["above_metabolic"], input_conc=input_c_n_ratios["above_metabolic"], old_pool_conc=self.data["c_n_ratio_above_metabolic"].to_numpy(), ) change_in_n_above_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["above_ground_structural"], + input_carbon=litter_inputs.input_above_structural, updated_pool_carbon=updated_pools["above_structural"], input_conc=input_c_n_ratios["above_structural"], old_pool_conc=self.data["c_n_ratio_above_structural"].to_numpy(), ) change_in_n_woody = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["woody"], + input_carbon=litter_inputs.input_woody, updated_pool_carbon=updated_pools["woody"], input_conc=input_c_n_ratios["woody"], old_pool_conc=self.data["c_n_ratio_woody"].to_numpy(), ) change_in_n_below_metabolic = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["below_ground_metabolic"], + input_carbon=litter_inputs.input_below_metabolic, updated_pool_carbon=updated_pools["below_metabolic"], input_conc=input_c_n_ratios["below_metabolic"], old_pool_conc=self.data["c_n_ratio_below_metabolic"].to_numpy(), ) change_in_n_below_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["below_ground_structural"], + input_carbon=litter_inputs.input_below_structural, updated_pool_carbon=updated_pools["below_structural"], input_conc=input_c_n_ratios["below_structural"], old_pool_conc=self.data["c_n_ratio_below_structural"].to_numpy(), @@ -494,7 +239,7 @@ def calculate_c_n_ratio_updates( def calculate_c_p_ratio_updates( self, - plant_inputs: dict[str, NDArray[np.float32]], + litter_inputs: LitterInputs, input_c_p_ratios: dict[str, NDArray[np.float32]], updated_pools: dict[str, NDArray[np.float32]], ) -> dict[str, NDArray[np.float32]]: @@ -504,8 +249,10 @@ def calculate_c_p_ratio_updates( be used in an integration process. Args: - plant_inputs: Dictionary containing the amount of each litter type that is - added from the plant model in this time step [kg C m^-2] + 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_c_p_ratios: Dictionary containing the carbon to phosphorus ratios of the input to each of the litter pools [unitless] updated_pools: Dictionary containing the updated pool densities for all 5 @@ -517,31 +264,31 @@ def calculate_c_p_ratio_updates( """ change_in_p_above_metabolic = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["above_ground_metabolic"], + input_carbon=litter_inputs.input_above_metabolic, updated_pool_carbon=updated_pools["above_metabolic"], input_conc=input_c_p_ratios["above_metabolic"], old_pool_conc=self.data["c_p_ratio_above_metabolic"].to_numpy(), ) change_in_p_above_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["above_ground_structural"], + input_carbon=litter_inputs.input_above_structural, updated_pool_carbon=updated_pools["above_structural"], input_conc=input_c_p_ratios["above_structural"], old_pool_conc=self.data["c_p_ratio_above_structural"].to_numpy(), ) change_in_p_woody = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["woody"], + input_carbon=litter_inputs.input_woody, updated_pool_carbon=updated_pools["woody"], input_conc=input_c_p_ratios["woody"], old_pool_conc=self.data["c_p_ratio_woody"].to_numpy(), ) change_in_p_below_metabolic = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["below_ground_metabolic"], + input_carbon=litter_inputs.input_below_metabolic, updated_pool_carbon=updated_pools["below_metabolic"], input_conc=input_c_p_ratios["below_metabolic"], old_pool_conc=self.data["c_p_ratio_below_metabolic"].to_numpy(), ) change_in_p_below_structural = calculate_change_in_chemical_concentration( - input_carbon=plant_inputs["below_ground_structural"], + input_carbon=litter_inputs.input_below_structural, updated_pool_carbon=updated_pools["below_structural"], input_conc=input_c_p_ratios["below_structural"], old_pool_conc=self.data["c_p_ratio_below_structural"].to_numpy(), @@ -660,6 +407,245 @@ def calculate_P_mineralisation( return total_P_mineralisation_rate / active_microbe_depth +def calculate_litter_input_lignin_concentrations( + litter_inputs: LitterInputs, +) -> dict[str, NDArray[np.float32]]: + """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 with the combined + total lignin content of the leaf and reproductive matter inputs being found, and + then converted to a back into a concentration. + + 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 kg C^-1] + """ + + lignin_proportion_woody = litter_inputs.deadwood_lignin + + lignin_proportion_below_structural = ( + litter_inputs.root_lignin + * litter_inputs.root_mass + / litter_inputs.input_below_structural + ) + + lignin_proportion_above_structural = ( + (litter_inputs.leaf_lignin * litter_inputs.leaf_mass) + + (litter_inputs.reprod_lignin * litter_inputs.reprod_mass) + ) / litter_inputs.input_above_structural + + return { + "woody": lignin_proportion_woody, + "below_structural": lignin_proportion_below_structural, + "above_structural": lignin_proportion_above_structural, + } + + +def calculate_litter_input_nitrogen_ratios( + litter_inputs: LitterInputs, + struct_to_meta_nitrogen_ratio: float, +) -> dict[str, NDArray[np.float32]]: + """Calculate the carbon to nitrogen ratio for each plant biomass to litter flow. + + The ratio for the input to the woody litter pool just matches the ratio of the + deadwood input. For the below ground pools, the ratios of the flows from root + turnover into the metabolic and structural pools is calculated. A similar + approach is taken for the above ground metabolic and structural pools, but here + a weighted average of the two contributions to each pool (leaf and reproductive + tissue turnover) must be taken. + + 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. + struct_to_meta_nitrogen_ratio: Ratio of the carbon to nitrogen ratios of + structural vs metabolic litter pools [unitless] + + Returns: + Dictionary containing the carbon to nitrogen ratios of the input to each of + the pools [unitless] + """ + + # Calculate c_n_ratio split for each (non-wood) input biomass type + root_c_n_ratio_meta, root_c_n_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.root_nitrogen, + metabolic_split=litter_inputs.roots_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, + ) + ) + + leaf_c_n_ratio_meta, leaf_c_n_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.leaf_nitrogen, + metabolic_split=litter_inputs.leaves_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, + ) + ) + + reprod_c_n_ratio_meta, reprod_c_n_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.reprod_nitrogen, + metabolic_split=litter_inputs.reproduct_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_nitrogen_ratio, + ) + ) + + c_n_ratio_below_metabolic = root_c_n_ratio_meta + c_n_ratio_below_structural = root_c_n_ratio_struct + c_n_ratio_woody = litter_inputs.deadwood_nitrogen + # Inputs with multiple sources have to be weighted + c_n_ratio_above_metabolic = np.divide( + ( + leaf_c_n_ratio_meta + * litter_inputs.leaf_mass + * litter_inputs.leaves_meta_split + ) + + ( + reprod_c_n_ratio_meta + * litter_inputs.reprod_mass + * litter_inputs.reproduct_meta_split + ), + (litter_inputs.leaf_mass * litter_inputs.leaves_meta_split) + + (litter_inputs.reprod_mass * litter_inputs.reproduct_meta_split), + ) + + c_n_ratio_above_structural = np.divide( + ( + leaf_c_n_ratio_struct + * litter_inputs.leaf_mass + * (1 - litter_inputs.leaves_meta_split) + ) + + ( + reprod_c_n_ratio_struct + * litter_inputs.reprod_mass + * (1 - litter_inputs.reproduct_meta_split) + ), + (litter_inputs.leaf_mass * (1 - litter_inputs.leaves_meta_split)) + + (litter_inputs.reprod_mass * (1 - litter_inputs.reproduct_meta_split)), + ) + + return { + "woody": c_n_ratio_woody, + "below_metabolic": c_n_ratio_below_metabolic, + "below_structural": c_n_ratio_below_structural, + "above_metabolic": c_n_ratio_above_metabolic, + "above_structural": c_n_ratio_above_structural, + } + + +def calculate_litter_input_phosphorus_ratios( + litter_inputs: LitterInputs, + struct_to_meta_phosphorus_ratio: float, +) -> dict[str, NDArray[np.float32]]: + """Calculate carbon to phosphorus ratio for each plant biomass to litter flow. + + The ratio for the input to the woody litter pool just matches the ratio of the + deadwood input. For the below ground pools, the ratios of the flows from root + turnover into the metabolic and structural pools is calculated. A similar approach + is taken for the above ground metabolic and structural pools, but here a weighted + average of the two contributions to each pool (leaf and reproductive tissue + turnover) must be taken. + + 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. + struct_to_meta_phosphorus_ratio: Ratio of the carbon to phosphorus ratios of + structural vs metabolic litter pools [unitless] + + Returns: + Dictionary containing the carbon to phosphorus ratios of the input to each of + the pools [unitless] + """ + + # Calculate c_p_ratio split for each (non-wood) input biomass type + root_c_p_ratio_meta, root_c_p_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.root_phosphorus, + metabolic_split=litter_inputs.roots_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, + ) + ) + + leaf_c_p_ratio_meta, leaf_c_p_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.leaf_phosphorus, + metabolic_split=litter_inputs.leaves_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, + ) + ) + + reprod_c_p_ratio_meta, reprod_c_p_ratio_struct = ( + calculate_nutrient_split_between_litter_pools( + input_c_nut_ratio=litter_inputs.reprod_phosphorus, + metabolic_split=litter_inputs.reproduct_meta_split, + struct_to_meta_nutrient_ratio=struct_to_meta_phosphorus_ratio, + ) + ) + + c_p_ratio_below_metabolic = root_c_p_ratio_meta + c_p_ratio_below_structural = root_c_p_ratio_struct + c_p_ratio_woody = litter_inputs.deadwood_phosphorus + # Inputs with multiple sources have to be weighted + c_p_ratio_above_metabolic = np.divide( + ( + leaf_c_p_ratio_meta + * litter_inputs.leaf_mass + * litter_inputs.leaves_meta_split + ) + + ( + reprod_c_p_ratio_meta + * litter_inputs.reprod_mass + * litter_inputs.reproduct_meta_split + ), + (litter_inputs.leaf_mass * litter_inputs.leaves_meta_split) + + (litter_inputs.reprod_mass * litter_inputs.reproduct_meta_split), + ) + + c_p_ratio_above_structural = np.divide( + ( + leaf_c_p_ratio_struct + * litter_inputs.leaf_mass + * (1 - litter_inputs.leaves_meta_split) + ) + + ( + reprod_c_p_ratio_struct + * litter_inputs.reprod_mass + * (1 - litter_inputs.reproduct_meta_split) + ), + (litter_inputs.leaf_mass * (1 - litter_inputs.leaves_meta_split)) + + (litter_inputs.reprod_mass * (1 - litter_inputs.reproduct_meta_split)), + ) + + return { + "woody": c_p_ratio_woody, + "below_metabolic": c_p_ratio_below_metabolic, + "below_structural": c_p_ratio_below_structural, + "above_metabolic": c_p_ratio_above_metabolic, + "above_structural": c_p_ratio_above_structural, + } + + def calculate_litter_chemistry_factor( lignin_proportion: NDArray[np.float32], lignin_inhibition_factor: float ) -> NDArray[np.float32]: diff --git a/virtual_ecosystem/models/litter/input_partition.py b/virtual_ecosystem/models/litter/input_partition.py deleted file mode 100644 index 612125db9..000000000 --- a/virtual_ecosystem/models/litter/input_partition.py +++ /dev/null @@ -1,202 +0,0 @@ -"""The ``models.litter.input_partition`` module handles the partitioning of dead plant -matter into the various pools of the litter model. -""" # noqa: D205 - -import numpy as np -from numpy.typing import NDArray - -from virtual_ecosystem.core.logger import LOGGER -from virtual_ecosystem.models.litter.constants import LitterConsts - -# TODO - It makes sense for the animal pools to be handled here, but need to think about -# how the partition works with the plant partition, Animals do not contain lignin, so if -# I used the standard function on animal carcasses and excrement the maximum amount -# (85%) will end up in the metabolic pool, which I think is basically fine, with bones -# not being explicitly modelled I think this is fine. This will have to change once -# bones are included. - - -def calculate_metabolic_proportions_of_input( - leaf_turnover_lignin_proportion: NDArray[np.float32], - reproduct_turnover_lignin_proportion: NDArray[np.float32], - root_turnover_lignin_proportion: NDArray[np.float32], - leaf_turnover_c_n_ratio: NDArray[np.float32], - reproduct_turnover_c_n_ratio: NDArray[np.float32], - root_turnover_c_n_ratio: NDArray[np.float32], - leaf_turnover_c_p_ratio: NDArray[np.float32], - reproduct_turnover_c_p_ratio: NDArray[np.float32], - root_turnover_c_p_ratio: NDArray[np.float32], - constants: LitterConsts, -) -> dict[str, NDArray[np.float32]]: - """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: - leaf_turnover_lignin_proportion: Proportion of carbon in turned over leaves that - is lignin [kg lignin kg C^-1] - reproduct_turnover_lignin_proportion: Proportion of carbon in turned over - reproductive tissues that is lignin [kg lignin kg C^-1] - root_turnover_lignin_proportion: Proportion of carbon in turned over roots that - is lignin [kg lignin kg C^-1] - leaf_turnover_c_n_ratio: Carbon:nitrogen ratio of turned over leaves [unitless] - reproduct_turnover_c_n_ratio: Carbon:nitrogen ratio of turned over reproductive - tissues [unitless] - root_turnover_c_n_ratio: Carbon:nitrogen ratio of turned over roots [unitless] - leaf_turnover_c_p_ratio: Carbon:phosphorus ratio of turned over leaves - [unitless] - reproduct_turnover_c_p_ratio: Carbon:phosphorus ratio of turned over - reproductive tissues [unitless] - root_turnover_c_p_ratio: Carbon:phosphorus ratio of turned over roots [unitless] - 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( - lignin_proportion=leaf_turnover_lignin_proportion, - carbon_nitrogen_ratio=leaf_turnover_c_n_ratio, - carbon_phosphorus_ratio=leaf_turnover_c_p_ratio, - 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, - ) - repoduct_metabolic_split = split_pool_into_metabolic_and_structural_litter( - lignin_proportion=reproduct_turnover_lignin_proportion, - carbon_nitrogen_ratio=reproduct_turnover_c_n_ratio, - carbon_phosphorus_ratio=reproduct_turnover_c_p_ratio, - 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( - lignin_proportion=root_turnover_lignin_proportion, - carbon_nitrogen_ratio=root_turnover_c_n_ratio, - carbon_phosphorus_ratio=root_turnover_c_p_ratio, - 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": leaves_metabolic_split, - "reproductive": repoduct_metabolic_split, - "roots": roots_metabolic_split, - } - - -def partion_plant_inputs_between_pools( - deadwood_production: NDArray[np.float32], - leaf_turnover: NDArray[np.float32], - reproduct_turnover: NDArray[np.float32], - root_turnover: NDArray[np.float32], - metabolic_splits: dict[str, NDArray[np.float32]], -): - """Function to partition input biomass between the various litter pools. - - All deadwood is added to the woody litter pool. Reproductive biomass (fruits and - flowers) and leaves are 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: - deadwood_production: Amount of dead wood produced [kg C m^-2] - leaf_turnover: Amount of leaf turnover [kg C m^-2] - reproduct_turnover: Turnover of plant reproductive tissues (i.e. fruits and - flowers) [kg C m^-2] - root_turnover: Turnover of roots (coarse and fine) turnover [kg C m^-2] - 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 biomass flow into each of the five litter pools - (woody, above ground metabolic, above ground structural, below ground metabolic - and below ground structural) - """ - - # Calculate input to each of the five litter pools - woody_input = deadwood_production - above_ground_metabolic_input = ( - metabolic_splits["leaves"] * leaf_turnover - + metabolic_splits["reproductive"] * reproduct_turnover - ) - above_ground_strutural_input = ( - (1 - metabolic_splits["leaves"]) * leaf_turnover - + (1 - metabolic_splits["reproductive"]) * reproduct_turnover - ) # fmt: off - below_ground_metabolic_input = metabolic_splits["roots"] * root_turnover - below_ground_structural_input = (1 - metabolic_splits["roots"]) * root_turnover - - return { - "woody": woody_input, - "above_ground_metabolic": above_ground_metabolic_input, - "above_ground_structural": above_ground_strutural_input, - "below_ground_metabolic": below_ground_metabolic_input, - "below_ground_structural": below_ground_structural_input, - } - - -def split_pool_into_metabolic_and_structural_litter( - lignin_proportion: NDArray[np.float32], - carbon_nitrogen_ratio: NDArray[np.float32], - carbon_phosphorus_ratio: NDArray[np.float32], - max_metabolic_fraction: float, - split_sensitivity_nitrogen: float, - split_sensitivity_phosphorus: float, -) -> NDArray[np.float32]: - """Calculate the split of input biomass between metabolic and structural pools. - - This division depends on the lignin and nitrogen content of the input biomass, the - functional form is taken from :cite:t:`parton_dynamics_1988`. - - Args: - lignin_proportion: Proportion of input biomass carbon that is lignin [kg lignin - kg C^-1] - carbon_nitrogen_ratio: Ratio of carbon to nitrogen for the input biomass - [unitless] - carbon_phosphorus_ratio: Ratio of carbon to phosphorus for the input biomass - [unitless] - 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 of the metabolic fractions drop below zero, or if any - structural fraction is less than the lignin proportion (which would push the - lignin proportion of the structural litter input above 100%). - - Returns: - The fraction of the biomass that goes to the metabolic pool [unitless] - """ - - metabolic_fraction = max_metabolic_fraction - lignin_proportion * ( - split_sensitivity_nitrogen * carbon_nitrogen_ratio - + split_sensitivity_phosphorus * carbon_phosphorus_ratio - ) - - if np.any(metabolic_fraction < 0.0): - to_raise = ValueError( - "Fraction of input biomass going to metabolic pool has dropped below zero!" - ) - LOGGER.error(to_raise) - raise to_raise - elif np.any(1 - metabolic_fraction < lignin_proportion): - to_raise = ValueError( - "Fraction of input biomass going to structural biomass is less than the " - "lignin fraction!" - ) - LOGGER.error(to_raise) - raise to_raise - else: - return metabolic_fraction diff --git a/virtual_ecosystem/models/litter/inputs.py b/virtual_ecosystem/models/litter/inputs.py new file mode 100644 index 000000000..4f01c8f21 --- /dev/null +++ b/virtual_ecosystem/models/litter/inputs.py @@ -0,0 +1,349 @@ +"""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 virtual_ecosystem.core.data import Data +from virtual_ecosystem.core.logger import LOGGER +from virtual_ecosystem.models.litter.constants import LitterConsts + + +@dataclass(frozen=True) +class LitterInputs: + """The full set input flows to the litter model.""" + + leaf_mass: NDArray[np.float32] + """Total leaf input mass to litter [kg C m^-2]""" + root_mass: NDArray[np.float32] + """Total root input mass to litter [kg C m^-2]""" + deadwood_mass: NDArray[np.float32] + """Total deadwood input mass to litter [kg C m^-2]""" + reprod_mass: NDArray[np.float32] + """Total plant reproductive tissue input mass to litter [kg C m^-2]""" + + leaf_lignin: NDArray[np.float32] + """Lignin proportion of leaf input [unitless]""" + root_lignin: NDArray[np.float32] + """Lignin proportion of root input [unitless]""" + deadwood_lignin: NDArray[np.float32] + """Lignin proportion of deadwood input [unitless]""" + reprod_lignin: NDArray[np.float32] + """Lignin proportion of reproductive tissue input [unitless]""" + + leaf_nitrogen: NDArray[np.float32] + """Carbon nitrogen ratio of leaf input [unitless]""" + root_nitrogen: NDArray[np.float32] + """Carbon nitrogen ratio of root input [unitless]""" + deadwood_nitrogen: NDArray[np.float32] + """Carbon nitrogen ratio of deadwood input [unitless]""" + reprod_nitrogen: NDArray[np.float32] + """Carbon nitrogen ratio of reproductive tissue input [unitless]""" + + leaf_phosphorus: NDArray[np.float32] + """Carbon phosphorus ratio of leaf input [unitless]""" + root_phosphorus: NDArray[np.float32] + """Carbon phosphorus ratio of root input [unitless]""" + deadwood_phosphorus: NDArray[np.float32] + """Carbon phosphorus ratio of deadwood input [unitless]""" + reprod_phosphorus: NDArray[np.float32] + """Carbon phosphorus ratio of reproductive tissue input [unitless]""" + + leaves_meta_split: NDArray[np.float32] + """Fraction of leaf input that goes to metabolic litter [unitless]""" + reproduct_meta_split: NDArray[np.float32] + """Fraction of leaf input that goes to metabolic litter [unitless]""" + roots_meta_split: NDArray[np.float32] + """Fraction of leaf input that goes to metabolic litter [unitless]""" + + input_woody: NDArray[np.float32] + """Total input to the woody litter pool [kg C m^-2]""" + input_above_metabolic: NDArray[np.float32] + """Total input to the above ground metabolic litter pool [kg C m^-2]""" + input_above_structural: NDArray[np.float32] + """Total input to the above ground structural litter pool [kg C m^-2]""" + input_below_metabolic: NDArray[np.float32] + """Total input to the below ground metabolic litter pool [kg C m^-2]""" + input_below_structural: NDArray[np.float32] + """Total input to the below ground structural litter pool [kg C m^-2]""" + + @classmethod + def create_from_data(cls, data: Data, constants: LitterConsts) -> 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 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. + + 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) + + # 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) + + +def combine_input_sources(data: Data) -> dict[str, NDArray[np.float32]]: + """Combine the plant death and herbivory inputs into a single total input. + + The total input for each plant matter type (leaves, roots, deadwood, + reproductive tissue) is returned, the chemical concentration of each of these + new pools is also calculated. + + TODO - At the moment there is only leaf input defined so this function doesn't + really do anything for the other types of plant matter. Once input is defined + for them this function should be updated to actually do something with them. + + Args: + data: The `Data` object to be used to populate the litter input streams. + + Returns: + A dictionary containing the total pool size for each input pools [kg C + m^-3], as well as the chemistry proportions (lignin, nitrogen and + phosphorus) of each of these pools [unitless]. + """ + + # Calculate totals for each plant matter type + leaf_total = data["leaf_turnover"] + data["herbivory_waste_leaf_carbon"] + root_total = data["root_turnover"] + deadwood_total = data["deadwood_production"] + reprod_total = data["plant_reproductive_tissue_turnover"] + + # Calculate leaf lignin concentrations for each combined pool + leaf_lignin = ( + data["leaf_turnover_lignin"] * data["leaf_turnover"] + + data["herbivory_waste_leaf_lignin"] * data["herbivory_waste_leaf_carbon"] + ) / (leaf_total) + root_lignin = data["root_turnover_lignin"] + deadwood_lignin = data["deadwood_lignin"] + reprod_lignin = data["plant_reproductive_tissue_turnover_lignin"] + + # Calculate leaf nitrogen concentrations for each combined pool + leaf_nitrogen = ( + data["leaf_turnover_c_n_ratio"] * data["leaf_turnover"] + + data["herbivory_waste_leaf_nitrogen"] * data["herbivory_waste_leaf_carbon"] + ) / (leaf_total) + root_nitrogen = data["root_turnover_c_n_ratio"] + deadwood_nitrogen = data["deadwood_c_n_ratio"] + reprod_nitrogen = data["plant_reproductive_tissue_turnover_c_n_ratio"] + + # Calculate leaf phosphorus concentrations for each combined pool + leaf_phosphorus = ( + data["leaf_turnover_c_p_ratio"] * data["leaf_turnover"] + + data["herbivory_waste_leaf_phosphorus"] * data["herbivory_waste_leaf_carbon"] + ) / (leaf_total) + root_phosphorus = data["root_turnover_c_p_ratio"] + deadwood_phosphorus = data["deadwood_c_p_ratio"] + reprod_phosphorus = data["plant_reproductive_tissue_turnover_c_p_ratio"] + + return { + "leaf_mass": leaf_total.to_numpy(), + "root_mass": root_total.to_numpy(), + "deadwood_mass": deadwood_total.to_numpy(), + "reprod_mass": reprod_total.to_numpy(), + "leaf_lignin": leaf_lignin.to_numpy(), + "root_lignin": root_lignin.to_numpy(), + "deadwood_lignin": deadwood_lignin.to_numpy(), + "reprod_lignin": reprod_lignin.to_numpy(), + "leaf_nitrogen": leaf_nitrogen.to_numpy(), + "root_nitrogen": root_nitrogen.to_numpy(), + "deadwood_nitrogen": deadwood_nitrogen.to_numpy(), + "reprod_nitrogen": reprod_nitrogen.to_numpy(), + "leaf_phosphorus": leaf_phosphorus.to_numpy(), + "root_phosphorus": root_phosphorus.to_numpy(), + "deadwood_phosphorus": deadwood_phosphorus.to_numpy(), + "reprod_phosphorus": reprod_phosphorus.to_numpy(), + } + + +def calculate_metabolic_proportions_of_input( + total_input: dict[str, NDArray[np.float32]], constants: LitterConsts +) -> dict[str, NDArray[np.float32]]: + """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 pool size for each input pool [kg C m^-3], as well as + the chemical proportions (lignin, nitrogen and phosphorus) of each of these + pools [unitless]. + 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( + lignin_proportion=total_input["leaf_lignin"], + carbon_nitrogen_ratio=total_input["leaf_nitrogen"], + carbon_phosphorus_ratio=total_input["leaf_phosphorus"], + 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, + ) + + repoduct_metabolic_split = split_pool_into_metabolic_and_structural_litter( + lignin_proportion=total_input["reprod_lignin"], + carbon_nitrogen_ratio=total_input["reprod_nitrogen"], + carbon_phosphorus_ratio=total_input["reprod_phosphorus"], + 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( + lignin_proportion=total_input["root_lignin"], + carbon_nitrogen_ratio=total_input["root_nitrogen"], + carbon_phosphorus_ratio=total_input["root_phosphorus"], + 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, + "reproduct_meta_split": repoduct_metabolic_split, + "roots_meta_split": roots_metabolic_split, + } + + +def partion_plant_inputs_between_pools( + total_input: dict[str, NDArray[np.float32]], + metabolic_splits: dict[str, NDArray[np.float32]], +): + """Function to partition input biomass between the various litter pools. + + All deadwood is added to the woody litter pool. Reproductive biomass (fruits and + flowers) and leaves are 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 total pool size for each input pool [kg C m^-2], as well as + the chemical proportions (lignin, nitrogen and phosphorus) of each of + these pools [unitless]. + 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 biomass flow into each of the five litter pools + (woody, above ground metabolic, above ground structural, below ground + metabolic and below ground structural) + """ + + # Calculate input to each of the five litter pools + woody_input = total_input["deadwood_mass"] + above_ground_metabolic_input = ( + metabolic_splits["leaves_meta_split"] * total_input["leaf_mass"] + + metabolic_splits["reproduct_meta_split"] * total_input["reprod_mass"] + ) + above_ground_strutural_input = ( + 1 - metabolic_splits["leaves_meta_split"] + ) * total_input["leaf_mass"] + ( + 1 - metabolic_splits["reproduct_meta_split"] + ) * total_input["reprod_mass"] + below_ground_metabolic_input = ( + metabolic_splits["roots_meta_split"] * total_input["root_mass"] + ) + below_ground_structural_input = ( + 1 - metabolic_splits["roots_meta_split"] + ) * total_input["root_mass"] + + return { + "input_woody": woody_input, + "input_above_metabolic": above_ground_metabolic_input, + "input_above_structural": above_ground_strutural_input, + "input_below_metabolic": below_ground_metabolic_input, + "input_below_structural": below_ground_structural_input, + } + + +def split_pool_into_metabolic_and_structural_litter( + lignin_proportion: NDArray[np.float32], + carbon_nitrogen_ratio: NDArray[np.float32], + carbon_phosphorus_ratio: NDArray[np.float32], + max_metabolic_fraction: float, + split_sensitivity_nitrogen: float, + split_sensitivity_phosphorus: float, +) -> NDArray[np.float32]: + """Calculate the split of input biomass between metabolic and structural pools. + + This division depends on the lignin and nitrogen content of the input biomass, the + functional form is taken from :cite:t:`parton_dynamics_1988`. + + Args: + lignin_proportion: Proportion of input biomass carbon that is lignin [kg lignin + kg C^-1] + carbon_nitrogen_ratio: Ratio of carbon to nitrogen for the input biomass + [unitless] + carbon_phosphorus_ratio: Ratio of carbon to phosphorus for the input biomass + [unitless] + 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 of the metabolic fractions drop below zero, or if any + structural fraction is less than the lignin proportion (which would push the + lignin proportion of the structural litter input above 100%). + + Returns: + The fraction of the biomass that goes to the metabolic pool [unitless] + """ + + metabolic_fraction = max_metabolic_fraction - lignin_proportion * ( + split_sensitivity_nitrogen * carbon_nitrogen_ratio + + split_sensitivity_phosphorus * carbon_phosphorus_ratio + ) + + if np.any(metabolic_fraction < 0.0): + to_raise = ValueError( + "Fraction of input biomass going to metabolic pool has dropped below zero!" + ) + LOGGER.error(to_raise) + raise to_raise + elif np.any(1 - metabolic_fraction < lignin_proportion): + to_raise = ValueError( + "Fraction of input biomass going to structural biomass is less than the " + "lignin fraction!" + ) + LOGGER.error(to_raise) + raise to_raise + else: + return metabolic_fraction diff --git a/virtual_ecosystem/models/litter/litter_model.py b/virtual_ecosystem/models/litter/litter_model.py index e26297c62..6b2200a1f 100644 --- a/virtual_ecosystem/models/litter/litter_model.py +++ b/virtual_ecosystem/models/litter/litter_model.py @@ -16,9 +16,6 @@ class instance. If errors crop here when converting the information from the con be reported as one. """ # noqa: D205 -# TODO - At the moment this model only receives nothing from the animal model. In -# future, litter flows due to waste from herbivory need to be added. - # FUTURE - Potentially make a more numerically accurate version of this model by using # differential equations at some point. In reality, litter chemistry should change # continuously with time not just at the final time step as in the current @@ -49,10 +46,7 @@ class instance. If errors crop here when converting the information from the con ) from virtual_ecosystem.models.litter.chemistry import LitterChemistry from virtual_ecosystem.models.litter.constants import LitterConsts -from virtual_ecosystem.models.litter.input_partition import ( - calculate_metabolic_proportions_of_input, - partion_plant_inputs_between_pools, -) +from virtual_ecosystem.models.litter.inputs import LitterInputs class LitterModel( @@ -111,6 +105,14 @@ class LitterModel( "leaf_turnover_c_n_ratio", "plant_reproductive_tissue_turnover_c_n_ratio", "root_turnover_c_n_ratio", + "deadwood_c_p_ratio", + "leaf_turnover_c_p_ratio", + "plant_reproductive_tissue_turnover_c_p_ratio", + "root_turnover_c_p_ratio", + "herbivory_waste_leaf_carbon", + "herbivory_waste_leaf_nitrogen", + "herbivory_waste_leaf_phosphorus", + "herbivory_waste_leaf_lignin", "litter_consumption_above_metabolic", "litter_consumption_above_structural", "litter_consumption_woody", @@ -321,45 +323,15 @@ def update(self, time_index: int, **kwargs: Any) -> None: constants=self.model_constants, ) - # Find the plant inputs to each of the litter pools - metabolic_splits = calculate_metabolic_proportions_of_input( - leaf_turnover_lignin_proportion=self.data[ - "leaf_turnover_lignin" - ].to_numpy(), - reproduct_turnover_lignin_proportion=self.data[ - "plant_reproductive_tissue_turnover_lignin" - ].to_numpy(), - root_turnover_lignin_proportion=self.data[ - "root_turnover_lignin" - ].to_numpy(), - leaf_turnover_c_n_ratio=self.data["leaf_turnover_c_n_ratio"].to_numpy(), - reproduct_turnover_c_n_ratio=self.data[ - "plant_reproductive_tissue_turnover_c_n_ratio" - ].to_numpy(), - root_turnover_c_n_ratio=self.data["root_turnover_c_n_ratio"].to_numpy(), - leaf_turnover_c_p_ratio=self.data["leaf_turnover_c_p_ratio"].to_numpy(), - reproduct_turnover_c_p_ratio=self.data[ - "plant_reproductive_tissue_turnover_c_p_ratio" - ].to_numpy(), - root_turnover_c_p_ratio=self.data["root_turnover_c_p_ratio"].to_numpy(), - constants=self.model_constants, - ) - - plant_inputs = partion_plant_inputs_between_pools( - deadwood_production=self.data["deadwood_production"].to_numpy(), - leaf_turnover=self.data["leaf_turnover"].to_numpy(), - reproduct_turnover=self.data[ - "plant_reproductive_tissue_turnover" - ].to_numpy(), - root_turnover=self.data["root_turnover"].to_numpy(), - metabolic_splits=metabolic_splits, + litter_inputs = LitterInputs.create_from_data( + self.data, constants=self.model_constants ) # Calculate the updated pool masses updated_pools = calculate_updated_pools( post_consumption_pools=consumed_pools, decay_rates=decay_rates, - plant_inputs=plant_inputs, + litter_inputs=litter_inputs, update_interval=self.model_timing.update_interval_quantity.to( "day" ).magnitude, @@ -367,9 +339,7 @@ def update(self, time_index: int, **kwargs: Any) -> None: # Calculate all the litter chemistry changes updated_chemistries = self.litter_chemistry.calculate_new_pool_chemistries( - plant_inputs=plant_inputs, - metabolic_splits=metabolic_splits, - updated_pools=updated_pools, + updated_pools=updated_pools, litter_inputs=litter_inputs ) # Calculate the total mineralisation rates from the litter