diff --git a/tests/conftest.py b/tests/conftest.py index e19adef58..01aea7601 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -103,6 +103,8 @@ def microbial_groups_cfg(): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 [[soil.microbial_group_definition]] name = "fungi" @@ -121,6 +123,8 @@ def microbial_groups_cfg(): reference_temperature = 12.0 c_n_ratio = 6.5 c_p_ratio = 40.0 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 [[soil.enzyme_class_definition]] source = "bacteria" @@ -131,6 +135,8 @@ def microbial_groups_cfg(): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "bacteria" @@ -141,6 +147,8 @@ def microbial_groups_cfg(): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "fungi" @@ -151,6 +159,8 @@ def microbial_groups_cfg(): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 [[soil.enzyme_class_definition]] source = "fungi" @@ -161,6 +171,8 @@ def microbial_groups_cfg(): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 """ diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index b4d4ba0e3..4936aafb2 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -2,8 +2,6 @@ import pytest -from virtual_ecosystem.models.soil.env_factors import EnvironmentalEffectFactors - @pytest.fixture def fixture_soil_config(microbial_groups_cfg): @@ -51,38 +49,16 @@ def environmental_factors(dummy_carbon_data, fixture_core_components): """Environmental factors based on dummy carbon data.""" from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.env_factors import ( - calculate_clay_impact_on_enzyme_saturation, - calculate_pH_suitability, - calculate_water_potential_impact_on_microbes, + calculate_environmental_effect_factors, ) - soil_constants = SoilConsts() - - water_factors = calculate_water_potential_impact_on_microbes( - water_potential=dummy_carbon_data["matric_potential"][ + return calculate_environmental_effect_factors( + soil_water_potential=dummy_carbon_data["matric_potential"][ fixture_core_components.layer_structure.index_topsoil_scalar ].to_numpy(), - water_potential_halt=soil_constants.soil_microbe_water_potential_halt, - water_potential_opt=soil_constants.soil_microbe_water_potential_optimum, - response_curvature=soil_constants.microbial_water_response_curvature, - ) - - pH_factors = calculate_pH_suitability( - soil_pH=dummy_carbon_data["pH"].to_numpy(), - maximum_pH=soil_constants.max_pH_microbes, - minimum_pH=soil_constants.min_pH_microbes, - lower_optimum_pH=soil_constants.lowest_optimal_pH_microbes, - upper_optimum_pH=soil_constants.highest_optimal_pH_microbes, - ) - - clay_saturation_factors = calculate_clay_impact_on_enzyme_saturation( + pH=dummy_carbon_data["pH"].to_numpy(), clay_fraction=dummy_carbon_data["clay_fraction"].to_numpy(), - base_protection=soil_constants.base_soil_protection, - protection_with_clay=soil_constants.soil_protection_with_clay, - ) - - return EnvironmentalEffectFactors( - water=water_factors, pH=pH_factors, clay_saturation=clay_saturation_factors + constants=SoilConsts, ) @@ -189,13 +165,15 @@ def maom_desorption(dummy_carbon_data): @pytest.fixture -def functional_groups(fixture_config): +def functional_groups(fixture_config, enzyme_classes): """Set of functional groups based on the soil model constants.""" from virtual_ecosystem.models.soil.microbial_groups import ( make_full_set_of_microbial_groups, ) - return make_full_set_of_microbial_groups(config=fixture_config) + return make_full_set_of_microbial_groups( + config=fixture_config, enzyme_classes=enzyme_classes + ) @pytest.fixture @@ -208,6 +186,20 @@ def enzyme_classes(fixture_config): return make_full_set_of_enzymes(config=fixture_config) +@pytest.fixture +def enzyme_changes(soil_pool_data, enzyme_production, enzyme_classes): + """Changes for each each enzyme class.""" + from virtual_ecosystem.models.soil.pools import ( + calculate_enzyme_changes, + ) + + return calculate_enzyme_changes( + pools=soil_pool_data, + enzyme_production=enzyme_production, + enzyme_classes=enzyme_classes, + ) + + @pytest.fixture def biomass_losses(dummy_carbon_data, functional_groups, fixture_core_components): """Rates of biomass loss from each microbial pool.""" @@ -232,3 +224,62 @@ def biomass_losses(dummy_carbon_data, functional_groups, fixture_core_components ) return {"bacteria": bacterial_biomass_loss, "fungi": fungal_biomass_loss} + + +@pytest.fixture +def growth_rates( + environmental_factors, + functional_groups, + soil_pool_data, + dummy_carbon_data, + fixture_core_components, +): + """Fixture to store growth rates of all the microbial groups.""" + from virtual_ecosystem.models.soil.constants import SoilConsts + from virtual_ecosystem.models.soil.pools import calculate_nutrient_uptake_rates + + bacterial_growth, _ = calculate_nutrient_uptake_rates( + soil_c_pool_lmwc=soil_pool_data.soil_c_pool_lmwc, + soil_n_pool_don=soil_pool_data.soil_n_pool_don, + soil_n_pool_ammonium=soil_pool_data.soil_n_pool_ammonium, + soil_n_pool_nitrate=soil_pool_data.soil_n_pool_nitrate, + soil_p_pool_dop=soil_pool_data.soil_p_pool_dop, + soil_p_pool_labile=soil_pool_data.soil_p_pool_labile, + microbial_pool_size=soil_pool_data.soil_c_pool_bacteria, + water_factor=environmental_factors.water, + pH_factor=environmental_factors.pH, + soil_temp=dummy_carbon_data["soil_temperature"][ + fixture_core_components.layer_structure.index_topsoil_scalar + ], + constants=SoilConsts, + functional_group=functional_groups["bacteria"], + ) + fungal_growth, _ = calculate_nutrient_uptake_rates( + soil_c_pool_lmwc=soil_pool_data.soil_c_pool_lmwc, + soil_n_pool_don=soil_pool_data.soil_n_pool_don, + soil_n_pool_ammonium=soil_pool_data.soil_n_pool_ammonium, + soil_n_pool_nitrate=soil_pool_data.soil_n_pool_nitrate, + soil_p_pool_dop=soil_pool_data.soil_p_pool_dop, + soil_p_pool_labile=soil_pool_data.soil_p_pool_labile, + microbial_pool_size=soil_pool_data.soil_c_pool_fungi, + water_factor=environmental_factors.water, + pH_factor=environmental_factors.pH, + soil_temp=dummy_carbon_data["soil_temperature"][ + fixture_core_components.layer_structure.index_topsoil_scalar + ], + constants=SoilConsts, + functional_group=functional_groups["fungi"], + ) + + return {"bacteria": bacterial_growth, "fungi": fungal_growth} + + +@pytest.fixture +def enzyme_production(functional_groups, growth_rates): + """Fixture to store the total production rates for each enzyme class.""" + from virtual_ecosystem.models.soil.pools import calculate_enzyme_production + + return calculate_enzyme_production( + microbial_groups=functional_groups, + growth_rates=growth_rates, + ) diff --git a/tests/models/soil/test_env_factors.py b/tests/models/soil/test_env_factors.py index 9ac400ae9..3c1bcbc3a 100644 --- a/tests/models/soil/test_env_factors.py +++ b/tests/models/soil/test_env_factors.py @@ -37,7 +37,7 @@ def test_calculate_environmental_effect_factors( ) expected_water = [1.0, 0.94414168, 0.62176357, 0.07747536] - expected_pH = [0.25, 1.0, 0.428571428, 1.0] + expected_pH = [0.25, 1.0, 0.57142857, 1.0] expected_clay_sat = [1.782, 1.102, 0.83, 1.918] env_factors = calculate_environmental_effect_factors( @@ -131,8 +131,8 @@ def test_calculate_pH_suitability(): from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.env_factors import calculate_pH_suitability - pH_values = np.array([3.0, 7.5, 9.0, 5.7, 2.0, 11.5]) - expected_inhib = [0.25, 1.0, 0.428571428, 1.0, 0.0, 0.0] + pH_values = np.array([3.0, 7.5, 9.0, 10.0, 5.7, 2.0, 11.5]) + expected_inhib = [0.25, 1.0, 0.57142857, 0.28571428, 1.0, 0.0, 0.0] actual_inhib = calculate_pH_suitability( soil_pH=pH_values, diff --git a/tests/models/soil/test_microbial_groups.py b/tests/models/soil/test_microbial_groups.py index d5d28c4ee..1e24c7e65 100644 --- a/tests/models/soil/test_microbial_groups.py +++ b/tests/models/soil/test_microbial_groups.py @@ -11,7 +11,7 @@ from virtual_ecosystem.core.config import Config, ConfigurationError -def test_make_full_set_of_microbial_groups(fixture_config): +def test_make_full_set_of_microbial_groups(fixture_config, enzyme_classes): """Test that the function to make all the microbial group works.""" from virtual_ecosystem.models.soil.microbial_groups import ( MicrobialGroupConstants, @@ -20,7 +20,9 @@ def test_make_full_set_of_microbial_groups(fixture_config): expected_groups = ["bacteria", "fungi"] - functional_groups = make_full_set_of_microbial_groups(fixture_config) + functional_groups = make_full_set_of_microbial_groups( + fixture_config, enzyme_classes=enzyme_classes + ) assert set(expected_groups) == set(functional_groups.keys()) @@ -61,6 +63,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 """, [ ( @@ -90,6 +94,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 [[soil.microbial_group_definition]] name = "fungi" @@ -108,6 +114,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 [[soil.microbial_group_definition]] name = "archaea" @@ -126,6 +134,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 """, [ ( @@ -154,6 +164,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 [[soil.microbial_group_definition]] name = "archaea" @@ -172,6 +184,8 @@ def test_make_full_set_of_microbial_groups(fixture_config): reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 + enzyme_production.pom = 0.005 + enzyme_production.maom = 0.005 """, [ ( @@ -188,7 +202,9 @@ def test_make_full_set_of_microbial_groups(fixture_config): ), ], ) -def test_make_full_set_of_microbial_groups_errors(caplog, cfg_strings, exp_log): +def test_make_full_set_of_microbial_groups_errors( + caplog, enzyme_classes, cfg_strings, exp_log +): """Check that bad configs generate errors during microbial group generation.""" from virtual_ecosystem.models.soil.microbial_groups import ( make_full_set_of_microbial_groups, @@ -198,7 +214,7 @@ def test_make_full_set_of_microbial_groups_errors(caplog, cfg_strings, exp_log): caplog.clear() with pytest.raises(ConfigurationError): - _ = make_full_set_of_microbial_groups(config) + _ = make_full_set_of_microbial_groups(config, enzyme_classes=enzyme_classes) log_check(caplog, exp_log) @@ -247,6 +263,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "bacteria" @@ -257,7 +275,9 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 - + c_n_ratio = 5.2 + c_p_ratio = 16 + [[soil.enzyme_class_definition]] source = "fungi" substrate = "pom" @@ -267,6 +287,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 """, [ ( @@ -274,7 +296,7 @@ def test_make_full_set_of_enzymes(fixture_config): "The following expected enzyme classes are not defined: fungi_maom", ) ], - id="missing_most_fungi_maom", + id="missing_fungi_maom", ), pytest.param( # archaea included but they shouldn't be """ @@ -287,6 +309,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "bacteria" @@ -297,6 +321,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "fungi" @@ -307,6 +333,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 [[soil.enzyme_class_definition]] source = "fungi" @@ -317,6 +345,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 [[soil.enzyme_class_definition]] source = "fungi" @@ -327,6 +357,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 """, [ ( @@ -347,6 +379,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 5.2 + c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "fungi" @@ -357,6 +391,8 @@ def test_make_full_set_of_enzymes(fixture_config): activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 + c_n_ratio = 6.5 + c_p_ratio = 40.0 """, [ ( @@ -385,3 +421,45 @@ def test_make_full_set_of_enzymes_errors(caplog, cfg_strings, exp_log): _ = make_full_set_of_enzymes(config) log_check(caplog, exp_log) + + +def test_find_enzyme_substrates(fixture_config, enzyme_classes): + """Check method to find the full set of substrates a microbe can use works.""" + from virtual_ecosystem.models.soil.microbial_groups import MicrobialGroupConstants + + bacteria = MicrobialGroupConstants.build_microbial_group( + group_config=next( + functional_group + for functional_group in fixture_config["soil"]["microbial_group_definition"] + if functional_group["name"] == "bacteria" + ), + enzyme_classes=enzyme_classes, + ) + + assert set(bacteria.find_enzyme_substrates()) == set(["maom", "pom"]) + + +def test_calculate_new_biomass_average_nutrient_ratios(fixture_config, enzyme_classes): + """Check method to calculate average new biomass nutrient ratios works.""" + import numpy as np + + from virtual_ecosystem.models.soil.microbial_groups import ( + calculate_new_biomass_average_nutrient_ratios, + ) + + group_config = next( + functional_group + for functional_group in fixture_config["soil"]["microbial_group_definition"] + if functional_group["name"] == "bacteria" + ) + + averaged_nutrient_ratios = calculate_new_biomass_average_nutrient_ratios( + name=group_config["name"], + c_n_ratio=5.7, + c_p_ratio=15.5, + enzyme_production=group_config["enzyme_production"], + enzyme_classes=enzyme_classes, + ) + + assert np.isclose(averaged_nutrient_ratios["nitrogen"], 5.695) + assert np.isclose(averaged_nutrient_ratios["phosphorus"], 15.505) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index aa6d98c28..468a999a6 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -46,29 +46,29 @@ def test_calculate_all_pool_updates( ) change_in_pools = { - "soil_c_pool_lmwc": [0.1178918, 0.06892161, 0.24201873, 0.03425211], - "soil_c_pool_maom": [0.03734787, 0.00340136, 0.05698608, 0.07275546], - "soil_c_pool_bacteria": [-0.054361097, -0.022606231, -0.118911406, -0.00719517], - "soil_c_pool_fungi": [-0.0083255777, -0.0819293436, -0.022969005, -0.032666056], - "soil_c_pool_pom": [0.00021589, -0.02918772, -0.01844761, 0.00520792], - "soil_c_pool_necromass": [0.010156477, 0.093205884, 0.056842818, -0.057486698], - "soil_enzyme_pom_bacteria": [1.18e-8, 1.67e-8, 1.78e-9, -1.12e-8], - "soil_enzyme_maom_bacteria": [-0.0003101, -5.0959e-5, 0.00059907, -3.7211e-5], - "soil_enzyme_pom_fungi": [-0.00054216, 0.000716408, 7.989000e-5, 0.000222079], - "soil_enzyme_maom_fungi": [-0.00012453, 0.000690584, 0.000143562, 0.00027601], - "soil_n_pool_don": [0.00180422, 0.00529461, 0.00528487, 0.00240583], - "soil_n_pool_particulate": [-1.005585e-4, 4.899227e-5, 1.05432e-4, 4.723906e-6], - "soil_n_pool_necromass": [0.00924867, 0.00082919, 0.00790357, -0.00394389], - "soil_n_pool_maom": [0.00099458, 0.01041398, 0.01344595, 0.00772835], - "soil_n_pool_ammonium": [0.00075125671, 0.02001151359, 0.00039745, 0.000172988], - "soil_n_pool_nitrate": [-0.003295899, -0.003990944, -0.001045921, -0.000642911], - "soil_p_pool_dop": [2.02317351e-4, 1.64662024e-4, 1.97339686e-4, 9.62312501e-5], - "soil_p_pool_particulate": [6.77587e-6, -7.228e-6, -2.639346e-7, 1.898709e-7], - "soil_p_pool_necromass": [0.002900315, 0.003433899, 0.007390806, 0.000848771], - "soil_p_pool_maom": [5.44660113e-4, -6.28584725e-5, 4.635421e-4, 3.0913116e-4], + "soil_c_pool_lmwc": [0.1178918, 0.06892161, 0.2489136, 0.03425211], + "soil_c_pool_maom": [0.03734787, 0.00340136, 0.05447554, 0.07275546], + "soil_c_pool_bacteria": [-0.0543618, -0.0226100, -0.1185237, -0.0071954], + "soil_c_pool_fungi": [-0.00832584, -0.08196413, -0.02280126, -0.03266732], + "soil_c_pool_pom": [0.00021589, -0.02918772, -0.0247599, 0.00520792], + "soil_c_pool_necromass": [0.01141213, 0.09537438, 0.05971513, -0.0566864], + "soil_enzyme_pom_bacteria": [-5.4395e-4, -2.2795e-4, -1.19323e-3, -7.2103e-5], + "soil_enzyme_maom_bacteria": [-8.5406e-4, -2.7893e-4, -5.9417e-4, -1.093e-4], + "soil_enzyme_pom_fungi": [-0.00062555, -0.00012062, -0.00015158, -0.00010522], + "soil_enzyme_maom_fungi": [-2.07924e-4, -1.46441e-4, -8.79093e-5, -5.1289e-5], + "soil_n_pool_don": [0.00180422, 0.00529461, 0.00549027, 0.00240583], + "soil_n_pool_particulate": [-1.005585e-4, 4.899227e-5, 7.96678e-5, 4.723906e-6], + "soil_n_pool_necromass": [0.00948372, 0.00118049, 0.00843786, -0.00381522], + "soil_n_pool_maom": [0.00099458, 0.01041398, 0.01326356, 0.00772835], + "soil_n_pool_ammonium": [0.00075125671, 0.02001151359, 0.00030892, 0.000172988], + "soil_n_pool_nitrate": [-0.003295899, -0.003990944, -0.00105915, -0.000642911], + "soil_p_pool_dop": [2.02317351e-4, 1.64662024e-4, 2.0799736e-4, 9.62312501e-5], + "soil_p_pool_particulate": [6.77587e-6, -7.228e-6, -1.29451192e-6, 1.898709e-7], + "soil_p_pool_necromass": [0.00297253, 0.00350535, 0.00755271, 0.0008742], + "soil_p_pool_maom": [5.44660113e-4, -6.28584725e-5, 4.528133e-4, 3.0913116e-4], "soil_p_pool_primary": [-4.473516e-10, -1.222973e-9, -6.33411e-10, -1.3674e-10], "soil_p_pool_secondary": [-5.050797e-7, -2.77311e-6, -7.40324e-7, -2.187697e-7], - "soil_p_pool_labile": [-1.643259e-5, -0.000295103, -9.270421e-5, -1.313285e-6], + "soil_p_pool_labile": [-1.643259e-5, -0.000295103, -1.212019e-4, -1.313285e-6], } # Make order of pools object @@ -148,21 +148,21 @@ def test_calculate_microbial_changes( from virtual_ecosystem.models.soil.pools import calculate_microbial_changes expected_mic_changes = { - "lmwc_uptake": [0.0002678173772, 0.01178568902, 0.00578389729, 0.0003198594108], - "don_uptake": [3.060766962e-6, 0.0008418340883, 8.26229727e-6, 0.0001827767514], - "ammonium_change": [3.32661813e-6, -0.0002060563, 0.00026558523, -0.0001422814], - "nitrate_change": [1.11256765e-5, -2.2895145e-5, 3.96851965e-5, -1.5809046e-5], - "dop_uptake": [3.060616979e-8, 1.346934537e-5, 3.30508087e-6, 7.31107003e-6], - "labile_p_change": [4.99284714e-6, 9.79315564e-5, 8.54931746e-5, -2.4335027e-6], - "bacteria_change": [-0.054361097, -0.022606231, -0.118911406, -0.007195167], - "fungi_change": [-0.0083255777, -0.0819293436, -0.022969005, -0.0326660561], - "pom_enzyme_bacteria_change": [1.18e-8, 1.67e-8, 1.78e-9, -1.12e-8], - "maom_enzyme_bacteria_change": [-0.0003101, -5.0959e-5, 0.00059907, -3.7211e-5], - "pom_enzyme_fungi_change": [-0.00054216, 0.000716408, 7.989000e-5, 0.000222079], - "maom_enzyme_fungi_change": [-0.00012453, 0.000690584, 0.000143562, 0.00027601], - "necromass_generation": [0.063759860, 0.107068803, 0.142793061, 0.039553892], - "necromass_n_flow": [0.011914627, 0.017358086, 0.026565221, 0.006364449], - "necromass_p_flow": [0.003646779, 0.003540533, 0.008051958, 0.001261101], + "lmwc_uptake": [0.0002678173772, 0.01178568902, 0.00771186303, 0.0003198594108], + "don_uptake": [3.060766962e-6, 0.0008418340883, 1.10163963e-5, 0.0001827767514], + "ammonium_change": [3.32661813e-6, -0.0002060563, 0.00035411364, -0.0001422814], + "nitrate_change": [1.11256765e-5, -2.2895145e-5, 5.29135951e-5, -1.5809046e-5], + "dop_uptake": [3.060616979e-8, 1.346934537e-5, 4.40677447e-6, 7.31107003e-6], + "labile_p_change": [4.99284714e-6, 9.79315564e-5, 1.13990898e-4, -2.4335027e-6], + "bacteria_change": [-0.0543618, -0.0226100, -0.1185237, -0.0071954], + "fungi_change": [-0.00832584, -0.08196413, -0.02280126, -0.03266732], + "pom_enzyme_bacteria_change": [-5.4395e-4, -2.2795e-4, -1.19323e-3, -7.2103e-5], + "maom_enzyme_bacteria_change": [-8.5406e-4, -2.7893e-4, -5.9417e-4, -1.093e-4], + "pom_enzyme_fungi_change": [-0.00062555, -0.00012062, -0.00015158, -0.00010522], + "maom_enzyme_fungi_change": [-2.07924e-4, -1.46441e-4, -8.79093e-5, -5.1289e-5], + "necromass_generation": [0.065015522, 0.109237318, 0.145665372, 0.040354202], + "necromass_n_flow": [0.01214967, 0.01770938, 0.02709952, 0.00649313], + "necromass_p_flow": [0.00371899, 0.00361198, 0.00821386, 0.00128653], } actual_mic_changes = calculate_microbial_changes( @@ -196,8 +196,8 @@ def test_calculate_enzyme_mediated_rates( from virtual_ecosystem.models.soil.pools import calculate_enzyme_mediated_rates expected_rates = { - "pom_to_lmwc": [0.001901992, 0.030246664, 0.018936876, 0.00028383], - "maom_to_lmwc": [0.00287967, 0.00699885, 0.00753161, 2.49959e-5], + "pom_to_lmwc": [0.001901992, 0.030246664, 0.0252492, 0.00028383], + "maom_to_lmwc": [0.00287967, 0.00699885, 0.0100421, 2.49959e-5], } actual_rates = calculate_enzyme_mediated_rates( @@ -283,26 +283,25 @@ def test_negative_nutrient_leaching(dummy_carbon_data, fixture_core_components): assert np.allclose(actual_leaching.labile_P, expected_labile_P) -def test_calculate_enzyme_changes( - dummy_carbon_data, soil_pool_data, biomass_losses, enzyme_classes -): +def test_calculate_enzyme_changes(soil_pool_data, enzyme_production, enzyme_classes): """Check that the determination of enzyme pool changes works correctly.""" from virtual_ecosystem.models.soil.pools import calculate_enzyme_changes expected_enzyme_changes = { - "net_change_pom_bacteria": [1.18e-8, 1.67e-8, 1.78e-9, -1.12e-8], - "net_change_maom_bacteria": [-0.0003101, -5.0959e-5, 0.00059907, -3.7211e-5], - "net_change_pom_fungi": [-0.00054216, 0.000716408, 7.989000e-5, 0.000222079], - "net_change_maom_fungi": [-0.00012453, 0.000690584, 0.000143562, 0.000276007], - "denaturation_bacterial": [0.001398696, 0.000510624, 0.001803384, 0.00018168], - "denaturation_fungal": [0.000833736, 0.000301824, 0.000246408, 0.000157752], + "net_change_pom_bacteria": [-0.00054395, -0.00022795, -0.00119323, -7.21028e-5], + "net_change_maom_bacteria": [-0.00085406, -0.00027893, -0.00059417, -0.0001093], + "net_change_pom_fungi": [-0.00062555, -0.00012062, -0.00015158, -0.00010522], + "net_change_maom_fungi": [-0.00020792, -0.00014644, -8.79093e-5, -5.12891e-5], + "denaturation_maom_bacteria": [0.0008544, 0.0002808, 0.00060216, 0.00010944], + "denaturation_pom_bacteria": [0.000544296, 0.000229824, 0.001201224, 7.224e-5], + "denaturation_maom_fungi": [0.000208056, 0.000163824, 9.1368e-5, 5.1912e-5], + "denaturation_pom_fungi": [0.00062568, 0.000138, 0.00015504, 0.00010584], } actual_enzyme_changes = calculate_enzyme_changes( pools=soil_pool_data, - biomass_losses=biomass_losses, - constants=SoilConsts, + enzyme_production=enzyme_production, enzyme_classes=enzyme_classes, ) @@ -316,18 +315,19 @@ def test_calculate_enzyme_changes( ) -def test_calculate_net_enzyme_change(dummy_carbon_data, biomass_losses, enzyme_classes): +def test_calculate_net_enzyme_change( + dummy_carbon_data, enzyme_production, enzyme_classes +): """Check that the determination of net enzyme pool change works correctly.""" from virtual_ecosystem.models.soil.pools import calculate_net_enzyme_change - expected_net_change = [1.18e-8, 1.67e-8, 1.78e-9, -1.12e-8] + expected_net_change = [-5.4395104e-4, -2.2795351e-4, -1.1932295e-3, -7.2102805e-5] expected_denat = [0.000544296, 0.000229824, 0.001201224, 7.224e-5] actual_net_change, actual_denat = calculate_net_enzyme_change( enzyme_pool_size=dummy_carbon_data["soil_enzyme_pom_bacteria"], - biomass_loss=biomass_losses["bacteria"], - biomass_loss_to_enzyme_class=SoilConsts.maintenance_bacteria_pom_enzyme, + enzyme_production=enzyme_production["bacteria_pom"], enzyme_turnover_rate=enzyme_classes["bacteria_pom"].turnover_rate, ) @@ -335,6 +335,27 @@ def test_calculate_net_enzyme_change(dummy_carbon_data, biomass_losses, enzyme_c assert np.allclose(actual_denat, expected_denat) +def test_calculate_enzyme_production(functional_groups, growth_rates): + """Test that the calculation of total enzyme production works as expected.""" + from virtual_ecosystem.models.soil.pools import calculate_enzyme_production + + expected_production = { + "bacteria_maom": [3.44963254e-7, 1.870490025e-6, 7.99454785e-6, 1.37195301e-7], + "bacteria_pom": [3.44963254e-7, 1.870490025e-6, 7.99454785e-6, 1.37195301e-7], + "fungi_maom": [1.323350415e-7, 1.738335842e-5, 3.4587141e-6, 6.22866665e-7], + "fungi_pom": [1.323350415e-7, 1.738335842e-5, 3.4587141e-6, 6.22866665e-7], + } + + actual_production = calculate_enzyme_production( + microbial_groups=functional_groups, growth_rates=growth_rates + ) + + assert expected_production.keys() == actual_production.keys() + + for enzyme in actual_production.keys(): + assert np.allclose(actual_production[enzyme], expected_production[enzyme]) + + def test_calculate_maintenance_biomass_synthesis( dummy_carbon_data, fixture_core_components, functional_groups ): @@ -411,14 +432,14 @@ def test_calculate_nutrient_uptake_rates( calculate_nutrient_uptake_rates, ) - expected_carbon_gain = [6.96825774e-5, 0.000377838985, 0.001211174, 2.77134508e-5] + expected_carbon_gain = [6.8992651e-5, 3.7409801e-4, 0.0015989096, 2.743906e-5] expected_consumption_rates = { - "organic_nitrogen": [2.2121431e-6, 8.17832483e-5, 5.76720686e-6, 3.29921934e-5], - "organic_phosphorus": [2.2120347e-8, 1.30853197e-6, 2.3069958e-6, 1.3196877e-6], - "carbon": [0.000193562715, 0.00114496662, 0.00403724667, 5.77363558e-5], - "inorganic_phosphorus": [4.333041e-6, 2.230641e-5, 7.339138e-5, 4.124029e-7], - "ammonium": [2.57532644e-6, -8.20971453e-6, 0.000197621226, -2.48964153e-5], - "nitrate": [8.6130261079e-6, -9.121905034e-7, 2.952964389e-5, -2.766268367e-6], + "organic_nitrogen": [2.2121431e-6, 8.1783248e-5, 7.6896091e-6, 3.2992193e-5], + "organic_phosphorus": [2.2120347e-8, 1.3085320e-6, 3.0759944e-6, 1.3196877e-6], + "carbon": [0.00019356272, 0.00114496662, 0.00538299554, 5.77363558e-5], + "inorganic_phosphorus": [4.333042e-6, 2.230641e-5, 9.785517e-5, 4.124029e-7], + "ammonium": [2.57532647e-6, -8.2097145e-6, 0.00026349497, -2.4896415e-5], + "nitrate": [8.61302608e-6, -9.1219050e-7, 3.93728584e-5, -2.7662684e-6], } actual_carbon_gain, actual_consumption_rates = calculate_nutrient_uptake_rates( @@ -459,7 +480,7 @@ def test_calculate_highest_achievable_nutrient_uptake( calculate_highest_achievable_nutrient_uptake, ) - expected_uptake = [1.29159055e-2, 8.43352433e-3, 5.77096991e-2, 5.77363558e-5] + expected_uptake = [1.29159055e-2, 8.43352433e-3, 7.6946265e-2, 5.77363558e-5] actual_uptake = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=dummy_carbon_data["soil_c_pool_lmwc"], @@ -495,7 +516,7 @@ def test_negative_highest_achievable_nutrient_uptake_are_impossible( labile_carbon_data[1] = -0.0001 labile_carbon_data[3] = -3.7e-5 - expected_uptake = [1.29159055e-2, 0.0, 5.77096991e-2, 0.0] + expected_uptake = [1.29159055e-2, 0.0, 7.6946265e-2, 0.0] actual_uptake = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=labile_carbon_data, @@ -527,7 +548,7 @@ def test_calculate_enzyme_mediated_decomposition( calculate_enzyme_mediated_decomposition, ) - expected_decomp = [3.39844565e-4, 8.91990315e-3, 1.25055119e-2, 4.14247999e-5] + expected_decomp = [3.39844565e-4, 8.91990315e-3, 1.66740158e-2, 4.14247999e-5] actual_decomp = calculate_enzyme_mediated_decomposition( soil_c_pool=dummy_carbon_data["soil_c_pool_pom"], @@ -665,7 +686,7 @@ def test_calculate_soil_nutrient_mineralisation( calculate_soil_nutrient_mineralisation, ) - expected_rate = [0.00013585646, 2.16036801e-5, 7.72932884e-5, 1.15848952e-5] + expected_rate = [0.00013585646, 2.16036801e-5, 1.030577177e-4, 1.15848952e-5] actual_rate = calculate_soil_nutrient_mineralisation( pool_carbon=dummy_carbon_data["soil_c_pool_pom"], @@ -676,7 +697,9 @@ def test_calculate_soil_nutrient_mineralisation( assert np.allclose(actual_rate, expected_rate) -def test_calculate_nutrient_flows_to_necromass(functional_groups): +def test_calculate_nutrient_flows_to_necromass( + functional_groups, enzyme_changes, enzyme_classes +): """Test that the function to calculate nutrient flows to necromass works.""" from virtual_ecosystem.models.soil.pools import ( calculate_nutrient_flows_to_necromass, @@ -687,13 +710,6 @@ def test_calculate_nutrient_flows_to_necromass(functional_groups): ) true_fungal_loss = np.array([0.008185263, 0.083731966, 0.023023140, 0.032136038]) - bacterial_enzyme_denaturation = np.array( - [0.001398696, 0.000510624, 0.001803384, 0.00018168] - ) - fungal_enzyme_denaturation = np.array( - [0.000833736, 0.000301824, 0.000246408, 0.000157752] - ) - expected_n_flow_to_necromass = [0.011914627, 0.017358086, 0.026565221, 0.006364449] expected_p_flow_to_necromass = [0.003646779, 0.003540533, 0.008051958, 0.001261101] @@ -701,9 +717,9 @@ def test_calculate_nutrient_flows_to_necromass(functional_groups): calculate_nutrient_flows_to_necromass( bacterial_loss=true_bacterial_loss, fungal_loss=true_fungal_loss, - bacterial_enzyme_denaturation=bacterial_enzyme_denaturation, - fungal_enzyme_denaturation=fungal_enzyme_denaturation, + enzyme_changes=enzyme_changes, microbial_groups=functional_groups, + enzyme_classes=enzyme_classes, ) ) @@ -747,8 +763,8 @@ def test_calculate_net_nutrient_transfers_from_maom_to_lmwc( ) expected_transfers = { - "nitrogen": [0.001004891, 0.001982692, 0.000550295, 2.911293e-6], - "phosphorus": [1.51879334e-5, 0.00014283379, 3.23215351e-5, 1.16451482e-7], + "nitrogen": [0.00100489, 0.00198269, 0.00073268, 2.91129e-6], + "phosphorus": [1.51879334e-5, 0.00014283379, 4.3050325e-5, 1.16451482e-7], } actual_transfers = calculate_net_nutrient_transfers_from_maom_to_lmwc( diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 4d94947eb..965c97570 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -350,68 +350,67 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.10865825, 0.05329372, 0.2206471, 0.02045373], dims="cell_id" + [0.10867866, 0.0528613, 0.2235931, 0.02047065], dims="cell_id" ), soil_c_pool_maom=DataArray( - [2.51943826, 1.70859259, 4.53271067, 0.53208277], dims="cell_id" + [2.519536, 1.70883297, 4.53172237, 0.53214258], dims="cell_id" ), soil_c_pool_bacteria=DataArray( - [5.77301845, 2.28884539, 11.24102147, 0.99642359], + [5.77301795, 2.28884242, 11.24128134, 0.99642333], dims="cell_id", ), soil_c_pool_fungi=DataArray( - [0.88589888, 8.51025597, 2.19873561, 4.52379402], + [0.88589868, 8.51022834, 2.1988625, 4.5237925], dims="cell_id", ), soil_c_pool_pom=DataArray( - [0.10011147, 0.9851898, 0.69082832, 0.35260191], dims="cell_id" + [0.1001131, 0.98559395, 0.68780336, 0.35260428], dims="cell_id" ), soil_c_pool_necromass=DataArray( - [0.06200223, 0.05221315, 0.11560307, 0.08195386], dims="cell_id" + [0.0625005, 0.05307367, 0.11674337, 0.08227162], dims="cell_id" ), soil_enzyme_pom_bacteria=DataArray( - [0.02267837, 0.00957573, 0.05004943, 0.00300993], dims="cell_id" + [0.02240932, 0.0094633, 0.04945988, 0.00297424], dims="cell_id" ), soil_enzyme_maom_bacteria=DataArray( - [0.03544525, 0.01167439, 0.02538618, 0.00454144], dims="cell_id" + [0.0351762, 0.01156196, 0.02479662, 0.00450575], dims="cell_id" ), soil_enzyme_pom_fungi=DataArray( - [0.02580045, 0.00610504, 0.00649941, 0.00452008], dims="cell_id" + [0.02575935, 0.00569559, 0.00638582, 0.00435819], dims="cell_id" ), soil_enzyme_maom_fungi=DataArray( - [0.00860701, 0.00716821, 0.00387805, 0.00229989], dims="cell_id" + [0.00856592, 0.00675875, 0.00376447, 0.00213799], dims="cell_id" ), soil_n_pool_don=DataArray( - [0.00169365, 0.00383974, 0.00294508, 0.00394796], dims="cell_id" + [0.00169773, 0.00382384, 0.00304483, 0.00395096], dims="cell_id" ), soil_n_pool_particulate=DataArray( - [0.0070931, 0.00073832, 0.00290946, 0.01428801], dims="cell_id" + [0.00709321, 0.00073862, 0.00289692, 0.01428811], dims="cell_id" ), soil_n_pool_necromass=DataArray( - [0.0065757, 0.01819928, 0.02332773, 0.00957007], dims="cell_id" + [0.00666897, 0.01833887, 0.02353984, 0.00962116], dims="cell_id" ), soil_n_pool_maom=DataArray( - [0.86657283, 0.48601413, 0.33422871, 0.099723], dims="cell_id" + [0.86659183, 0.48606279, 0.33418043, 0.09973263], dims="cell_id" ), soil_n_pool_ammonium=DataArray( - [0.0004275, 0.01509396, 0.00035874, 0.00524329], dims="cell_id" + [0.00042733, 0.01509539, 0.00030326, 0.00524331], dims="cell_id" ), soil_n_pool_nitrate=DataArray( - [0.00056245, 0.00203824, -0.00016233, 0.01271296], - dims="cell_id", + [0.0005624, 0.0020384, -0.0001656, 0.01271296], dims="cell_id" ), soil_p_pool_dop=DataArray( - [0.00017829, 0.00017471, 0.00033654, 0.00018269], dims="cell_id" + [0.00018004, 0.00017463, 0.00034475, 0.00018331], dims="cell_id" ), soil_p_pool_particulate=DataArray( - [3.19431193e-5, 2.82033889e-4, 1.14152879e-4, 5.71520842e-4], + [3.19436203e-5, 2.82149510e-4, 1.13656052e-4, 5.71524688e-4], dims="cell_id", ), soil_p_pool_necromass=DataArray( - [0.0019653, 0.00148675, 0.00366575, 0.00078505], dims="cell_id" + [0.00199395, 0.0015151, 0.00373002, 0.00079515], dims="cell_id" ), soil_p_pool_maom=DataArray( - [0.01356543, 0.03483871, 0.02001331, 0.00406402], dims="cell_id" + [0.01357081, 0.03484564, 0.02002014, 0.00406592], dims="cell_id" ), soil_p_pool_primary=DataArray( [0.0019594, 0.00535662, 0.00277434, 0.00059892], dims="cell_id" @@ -420,7 +419,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.00705642, 0.03816755, 0.0115255, 0.00733096], dims="cell_id" ), soil_p_pool_labile=DataArray( - [-6.65834883e-6, -1.33255769e-4, 2.35622831e-7, 1.91333142e-4], + [-6.7324868e-6, -1.331584e-4, -1.860591e-5, 1.9133659e-4], dims="cell_id", ), ) @@ -594,83 +593,83 @@ def test_construct_full_soil_model( delta_pools = [ 0.1178918, 0.06892161, - 0.24201873, + 0.2489136, 0.03425211, 0.03734787, 0.00340136, - 0.05698608, + 0.05447554, 0.07275546, - -0.054361097, - -0.022606231, - -0.118911406, - -0.007195167, - -0.0083255777, - -0.0819293436, - -0.022969005, - -0.032666056, + -0.0543618, + -0.0226100, + -0.1185237, + -0.0071954, + -0.00832584, + -0.08196413, + -0.02280126, + -0.03266732, 0.00021589, -0.02918772, - -0.01844761, + -0.0247599, 0.00520792, - 0.010156477, - 0.093205884, - 0.056842818, - -0.057486698, - 1.18e-8, - 1.67e-8, - 1.78e-9, - -1.12e-8, - -0.0003101, - -5.0959e-5, - 0.00059907, - -3.7211e-5, - -0.00054216, - 0.000716408, - 7.989000e-5, - 0.000222079, - -0.00012453, - 0.000690584, - 0.000143562, - 0.00027601, + 0.01141213, + 0.09537438, + 0.05971513, + -0.0566864, + -5.4395e-4, + -2.2795e-4, + -1.19323e-3, + -7.2103e-5, + -8.5406e-4, + -2.7893e-4, + -5.9417e-4, + -1.093e-4, + -0.00062555, + -0.00012062, + -0.00015158, + -0.00010522, + -2.07924e-4, + -1.46441e-4, + -8.79093e-5, + -5.1289e-5, 0.00180422, 0.00529461, - 0.00528487, + 0.00549027, 0.00240583, -1.005585e-4, 4.899227e-5, - 1.05432e-4, + 7.96678e-5, 4.723906e-6, - 0.00924867, - 0.00082919, - 0.00790357, - -0.00394389, + 0.00948372, + 0.00118049, + 0.00843786, + -0.00381522, 0.00099458, 0.01041398, - 0.01344595, + 0.01326356, 0.00772835, 0.00075125671, 0.02001151359, - 0.00039745, + 0.00030892, 0.000172988, -0.003295899, -0.003990944, - -0.001045921, + -0.00105915, -0.000642911, 2.02317351e-4, 1.64662024e-4, - 1.97339686e-4, + 2.0799736e-4, 9.62312501e-5, 6.77587e-6, -7.228e-6, - -2.639346e-7, + -1.29451192e-6, 1.898709e-7, - 0.002900315, - 0.003433899, - 0.007390806, - 0.000848771, + 0.00297253, + 0.00350535, + 0.00755271, + 0.0008742, 5.44660113e-4, -6.28584725e-5, - 4.635421e-4, + 4.528133e-4, 3.0913116e-4, -4.473516e-10, -1.222973e-9, @@ -682,7 +681,7 @@ def test_construct_full_soil_model( -2.187697e-7, -1.643259e-5, -0.000295103, - -9.270421e-5, + -1.212019e-4, -1.313285e-6, ] diff --git a/virtual_ecosystem/example_data/config/soil_microbial_groups.toml b/virtual_ecosystem/example_data/config/soil_microbial_groups.toml index 4be6226a2..87625dae1 100644 --- a/virtual_ecosystem/example_data/config/soil_microbial_groups.toml +++ b/virtual_ecosystem/example_data/config/soil_microbial_groups.toml @@ -15,6 +15,8 @@ activation_energy_turnover = 20000 reference_temperature = 12.0 c_n_ratio = 5.2 c_p_ratio = 16 +enzyme_production.pom = 0.005 +enzyme_production.maom = 0.005 [[soil.microbial_group_definition]] name = "fungi" @@ -33,6 +35,8 @@ activation_energy_turnover = 20000 reference_temperature = 12.0 c_n_ratio = 6.5 c_p_ratio = 40.0 +enzyme_production.pom = 0.005 +enzyme_production.maom = 0.005 [[soil.enzyme_class_definition]] source = "bacteria" @@ -43,6 +47,8 @@ activation_energy_rate = 37000 activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 +c_n_ratio = 5.2 +c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "bacteria" @@ -53,6 +59,8 @@ activation_energy_rate = 47000 activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 +c_n_ratio = 5.2 +c_p_ratio = 16 [[soil.enzyme_class_definition]] source = "fungi" @@ -63,6 +71,8 @@ activation_energy_rate = 37000 activation_energy_saturation = 30000 reference_temperature = 12.0 turnover_rate = 2.4e-2 +c_n_ratio = 6.5 +c_p_ratio = 40.0 [[soil.enzyme_class_definition]] source = "fungi" @@ -72,4 +82,6 @@ half_saturation_constant = 175.0 activation_energy_rate = 47000 activation_energy_saturation = 30000 reference_temperature = 12.0 -turnover_rate = 2.4e-2 \ No newline at end of file +turnover_rate = 2.4e-2 +c_n_ratio = 6.5 +c_p_ratio = 40.0 \ No newline at end of file diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 11b612d0a..a0f1de0fa 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -50,30 +50,6 @@ class SoilConsts(ConstantsDataclass): [unitless]. Value is taken from :cite:t:`moyano_responses_2013`. """ - maintenance_bacteria_pom_enzyme: float = 1e-2 - """Fraction of bacterial maintenance used to produce POM degrading enzymes. - - [unitless]. Value taken from :cite:t:`wang_development_2013`. - """ - - maintenance_fungi_pom_enzyme: float = 1e-2 - """Fraction of fungal maintenance used to produce POM degrading enzymes. - - [unitless]. Value taken from :cite:t:`wang_development_2013`. - """ - - maintenance_bacteria_maom_enzyme: float = 1e-2 - """Fraction of bacterial maintenance used to produce MAOM degrading enzymes. - - [unitless]. Value taken from :cite:t:`wang_development_2013`. - """ - - maintenance_fungi_maom_enzyme: float = 1e-2 - """Fraction of bacterial maintenance used to produce MAOM degrading enzymes. - - [unitless]. Value taken from :cite:t:`wang_development_2013`. - """ - # TODO - At some point, need to allow microbial and fungal environmental factors to # vary min_pH_microbes: float = 2.5 diff --git a/virtual_ecosystem/models/soil/env_factors.py b/virtual_ecosystem/models/soil/env_factors.py index eb307da64..70a12824c 100644 --- a/virtual_ecosystem/models/soil/env_factors.py +++ b/virtual_ecosystem/models/soil/env_factors.py @@ -209,9 +209,9 @@ def calculate_pH_suitability( lower_optimum_pH - minimum_pH ) # Linear decrease from the upper threshold to maximum pH - pH_factors[between_opt_and_max] = ( - soil_pH[between_opt_and_max] - upper_optimum_pH - ) / (maximum_pH - upper_optimum_pH) + pH_factors[between_opt_and_max] = (maximum_pH - soil_pH[between_opt_and_max]) / ( + maximum_pH - upper_optimum_pH + ) return pH_factors diff --git a/virtual_ecosystem/models/soil/microbial_groups.py b/virtual_ecosystem/models/soil/microbial_groups.py index 9038de4b6..1f1ac65e5 100644 --- a/virtual_ecosystem/models/soil/microbial_groups.py +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -3,6 +3,7 @@ """ # noqa: D205 from dataclasses import dataclass +from typing import Any from virtual_ecosystem.core.config import Config, ConfigurationError from virtual_ecosystem.core.logger import LOGGER @@ -40,6 +41,12 @@ class EnzymeConstants: turnover_rate: float """The turnover rate of the enzyme [day^-1].""" + c_n_ratio: float + """Ratio of carbon to nitrogen for the enzyme [unitless].""" + + c_p_ratio: float + """Ratio of carbon to phosphorus for the enzyme [unitless].""" + @dataclass(frozen=True) class MicrobialGroupConstants: @@ -99,14 +106,107 @@ class MicrobialGroupConstants: c_p_ratio: float """Ratio of carbon to phosphorus in biomass [unitless].""" + enzyme_production: dict[str, float] + """Details of the enzymes produced by the microbial group. + + The keys are the substrates for which enzymes are produced, and the values are the + allocation to enzyme production. This allocation is expressed as a fraction of the + (gross) cellular biomass growth. + """ + + synthesis_nutrient_ratios: dict[str, float] + """Average carbon to nutrient ratios for the total synthesised biomass. + + Microbes have to synthesis both cellular biomass and extracellular enzymes. We + assume that this occurs in fixed unvarying proportion. This attribute stores the + carbon nutrient (nitrogen, phosphorus) ratios for the total synthesised biomass. + """ + + @classmethod + def build_microbial_group( + cls, group_config: dict[str, Any], enzyme_classes: dict[str, EnzymeConstants] + ): + """Class method to build the microbial group including enzyme information. + + Args: + group_config: The config details for microbial group in question. + enzyme_classes: Details of the enzyme classes used by the soil model. + """ + + return cls( + **group_config, + synthesis_nutrient_ratios=calculate_new_biomass_average_nutrient_ratios( + name=group_config["name"], + c_n_ratio=group_config["c_n_ratio"], + c_p_ratio=group_config["c_p_ratio"], + enzyme_production=group_config["enzyme_production"], + enzyme_classes=enzyme_classes, + ), + ) + + def find_enzyme_substrates(self) -> list[str]: + """Substrates that the microbial group produces enzymes for.""" + + return [ + substrate + for substrate, production in self.enzyme_production.items() + if production > 0.0 + ] + + +def calculate_new_biomass_average_nutrient_ratios( + name: str, + c_n_ratio: float, + c_p_ratio: float, + enzyme_production: dict[str, float], + enzyme_classes: dict[str, EnzymeConstants], +) -> dict[str, float]: + """Calculate average carbon nutrient ratios of the newly synthesised biomass. + + Microbes have to synthesise cellular biomass as well as extracellular enzymes. This + method calculates average nutrient ratio of this total biomass synthesis by + calculating the average weighted by the relative production allocation to each + enzyme class and cellular growth. + + Args: + name: Name of the microbial group. + c_n_ratio: Ratio of carbon to nitrogen for the microbial group's cellular + biomass. + c_p_ratio: Ratio of carbon to nitrogen for the microbial group's cellular + biomass. + enzyme_production: Details of the enzymes produced by the microbial group, i.e. + which substrates are enzymes produced for, and how much (relative to + cellular synthesis) + enzyme_classes: Details of the enzyme classes used by the soil model. + """ + + enzyme_c_n_weighted = sum( + enzyme_classes[f"{name}_{substrate}"].c_n_ratio * allocation + for substrate, allocation in enzyme_production.items() + ) + + enzyme_c_p_weighted = sum( + enzyme_classes[f"{name}_{substrate}"].c_p_ratio * allocation + for substrate, allocation in enzyme_production.items() + ) + + total_enzyme_allocation = sum(enzyme_production.values()) + + return { + "nitrogen": (c_n_ratio + enzyme_c_n_weighted) / (1.0 + total_enzyme_allocation), + "phosphorus": (c_p_ratio + enzyme_c_p_weighted) + / (1.0 + total_enzyme_allocation), + } + def make_full_set_of_microbial_groups( - config: Config, + config: Config, enzyme_classes: dict[str, EnzymeConstants] ) -> dict[str, MicrobialGroupConstants]: """Make the full set of functional groups used in the soil model. Args: config: The complete virtual ecosystem config. + enzyme_classes: Details of the enzyme classes used by the soil model. Raises: ConfigurationError: If the soil model configuration is missing, if expected @@ -149,12 +249,13 @@ def make_full_set_of_microbial_groups( ) return { - group_name: MicrobialGroupConstants( - **next( + group_name: MicrobialGroupConstants.build_microbial_group( + group_config=next( functional_group for functional_group in config["soil"]["microbial_group_definition"] if functional_group["name"] == group_name - ) + ), + enzyme_classes=enzyme_classes, ) for group_name in expected_groups } diff --git a/virtual_ecosystem/models/soil/module_schema.json b/virtual_ecosystem/models/soil/module_schema.json index 5c0c112d6..720050a6c 100644 --- a/virtual_ecosystem/models/soil/module_schema.json +++ b/virtual_ecosystem/models/soil/module_schema.json @@ -46,6 +46,12 @@ }, "turnover_rate": { "type": "number" + }, + "c_n_ratio": { + "type": "number" + }, + "c_p_ratio": { + "type": "number" } }, "required": [ @@ -56,7 +62,9 @@ "activation_energy_rate", "activation_energy_saturation", "reference_temperature", - "turnover_rate" + "turnover_rate", + "c_n_ratio", + "c_p_ratio" ] } }, @@ -113,6 +121,22 @@ }, "c_p_ratio": { "type": "number" + }, + "enzyme_production": { + "description": "Allocation to production of each enzyme type", + "type": "object", + "properties": { + "pom": { + "type": "number" + }, + "maom": { + "type": "number" + } + }, + "required": [ + "pom", + "maom" + ] } }, "required": [ @@ -131,7 +155,8 @@ "activation_energy_turnover", "reference_temperature", "c_n_ratio", - "c_p_ratio" + "c_p_ratio", + "enzyme_production" ] } }, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 3cea519c7..f525fa55c 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -29,9 +29,6 @@ MicrobialGroupConstants, ) -# TODO - At this point in time I'm not adding specific phosphatase enzymes, need to -# think about adding these in future - @dataclass class MicrobialChanges: @@ -183,14 +180,26 @@ class EnzymePoolChanges: Units of [kg C m^-3 day^-1] """ - denaturation_bacterial: NDArray[np.float32] - """Total denaturation rate for all the enzyme produced by bacteria. + denaturation_pom_bacteria: NDArray[np.float32] + """Denaturation rate for the :term:`POM` degrading enzyme produced by bacteria. Units of [kg C m^-3 day^-1] """ - denaturation_fungal: NDArray[np.float32] - """Total denaturation rate for all the enzyme produced by fungi. + denaturation_maom_bacteria: NDArray[np.float32] + """Denaturation rate for the :term:`MAOM` degrading enzyme produced by bacteria. + + Units of [kg C m^-3 day^-1] + """ + + denaturation_pom_fungi: NDArray[np.float32] + """Denaturation rate for the :term:`POM` degrading enzyme produced by fungi. + + Units of [kg C m^-3 day^-1] + """ + + denaturation_maom_fungi: NDArray[np.float32] + """Denaturation rate for the :term:`MAOM` degrading enzyme produced by fungi. Units of [kg C m^-3 day^-1] """ @@ -780,36 +789,27 @@ def calculate_microbial_changes( soil_temp=soil_temp, microbial_group=functional_groups["fungi"], ) + + # Calculate the total production of each enzyme class + enzyme_production = calculate_enzyme_production( + microbial_groups=functional_groups, + growth_rates={"bacteria": bacterial_growth, "fungi": fungal_growth}, + ) + # Find changes in each enzyme pool enzyme_changes = calculate_enzyme_changes( pools=pools, - biomass_losses={ - "bacteria": bacterial_biomass_loss, - "fungi": fungal_biomass_loss, - }, - constants=constants, + enzyme_production=enzyme_production, enzyme_classes=enzyme_classes, ) - # Find fraction of loss that isn't enzyme production - true_bacterial_loss = ( - 1 - - constants.maintenance_bacteria_pom_enzyme - - constants.maintenance_bacteria_maom_enzyme - ) * bacterial_biomass_loss - true_fungal_loss = ( - 1 - - constants.maintenance_fungi_pom_enzyme - - constants.maintenance_fungi_maom_enzyme - ) * fungal_biomass_loss - # Find flow of nitrogen to necromass pool necromass_n_flow, necromass_p_flow = calculate_nutrient_flows_to_necromass( - bacterial_loss=true_bacterial_loss, - fungal_loss=true_fungal_loss, - bacterial_enzyme_denaturation=enzyme_changes.denaturation_bacterial, - fungal_enzyme_denaturation=enzyme_changes.denaturation_fungal, + bacterial_loss=bacterial_biomass_loss, + fungal_loss=fungal_biomass_loss, + enzyme_changes=enzyme_changes, microbial_groups=functional_groups, + enzyme_classes=enzyme_classes, ) return MicrobialChanges( @@ -830,10 +830,12 @@ def calculate_microbial_changes( pom_enzyme_fungi_change=enzyme_changes.net_change_pom_fungi, maom_enzyme_fungi_change=enzyme_changes.net_change_maom_fungi, necromass_generation=( - enzyme_changes.denaturation_bacterial - + enzyme_changes.denaturation_fungal - + true_bacterial_loss - + true_fungal_loss + enzyme_changes.denaturation_pom_bacteria + + enzyme_changes.denaturation_maom_bacteria + + enzyme_changes.denaturation_pom_fungi + + enzyme_changes.denaturation_maom_fungi + + bacterial_biomass_loss + + fungal_biomass_loss ), necromass_n_flow=necromass_n_flow, necromass_p_flow=necromass_p_flow, @@ -963,16 +965,14 @@ def calculate_nutrient_leaching( def calculate_enzyme_changes( pools: PoolData, - biomass_losses: dict[str, NDArray[np.float32]], - constants: SoilConsts, + enzyme_production: dict[str, NDArray[np.float32]], enzyme_classes: dict[str, EnzymeConstants], ) -> EnzymePoolChanges: """Calculate the change in each of the soil enzyme pools. Args: pools: Data class containing the various soil pools. - biomass_losses: Total rate of loss of biomass for each functional group due to - cell death, protein degradation and enzyme production [kg C m^-3 day^-1] + enzyme_production: Production rates for each class of enzyme [kg C m^-3 day^-1] constants: Set of constants for the soil model. enzyme_classes: Details of the enzyme classes used in the soil model. @@ -994,10 +994,7 @@ def calculate_enzyme_changes( enzyme_pool_size=getattr( pools, f"soil_enzyme_{substrate}_{source}" ), - biomass_loss=biomass_losses[source], - biomass_loss_to_enzyme_class=getattr( - constants, f"maintenance_{source}_{substrate}_enzyme" - ), + enzyme_production=enzyme_production[f"{source}_{substrate}"], enzyme_turnover_rate=enzyme_classes[ f"{source}_{substrate}" ].turnover_rate, @@ -1009,31 +1006,21 @@ def calculate_enzyme_changes( for source in sources } - total_denaturation = { - source: np.sum( - [ - enzyme_changes[source][substrate]["denaturation"] - for substrate in substrates - ], - axis=0, - ) - for source in sources - } - return EnzymePoolChanges( net_change_pom_bacteria=enzyme_changes["bacteria"]["pom"]["net_change"], net_change_maom_bacteria=enzyme_changes["bacteria"]["maom"]["net_change"], net_change_pom_fungi=enzyme_changes["fungi"]["pom"]["net_change"], net_change_maom_fungi=enzyme_changes["fungi"]["maom"]["net_change"], - denaturation_bacterial=total_denaturation["bacteria"], - denaturation_fungal=total_denaturation["fungi"], + denaturation_pom_bacteria=enzyme_changes["bacteria"]["pom"]["denaturation"], + denaturation_maom_bacteria=enzyme_changes["bacteria"]["maom"]["denaturation"], + denaturation_pom_fungi=enzyme_changes["fungi"]["pom"]["denaturation"], + denaturation_maom_fungi=enzyme_changes["fungi"]["maom"]["denaturation"], ) def calculate_net_enzyme_change( enzyme_pool_size: NDArray[np.float32], - biomass_loss: NDArray[np.float32], - biomass_loss_to_enzyme_class: float, + enzyme_production: NDArray[np.float32], enzyme_turnover_rate: float, ) -> tuple[NDArray[np.float32], NDArray[np.float32]]: """Calculate the change in concentration for a specific enzyme pool. @@ -1044,11 +1031,7 @@ def calculate_net_enzyme_change( Args: enzyme_pool_size: Amount of enzyme class of interest [kg C m^-3] - biomass_loss: Rate a which the microbial biomass pool of interest loses biomass, - this is a combination of enzyme excretion, protein degradation, and cell - death [kg C m^-3 day^-1] - biomass_loss_to_enzyme_class: Fraction of the biomass loss that goes to - producing the enzyme class of interest [unitless] + enzyme_production: Production rate for the enzyme in question [kg C m^-3 day^-1] enzyme_turnover_rate: Rate at which the enzyme denatures [day^-1] Returns: @@ -1057,7 +1040,6 @@ def calculate_net_enzyme_change( """ # Calculate production and turnover of each enzyme class - enzyme_production = biomass_loss_to_enzyme_class * biomass_loss enzyme_turnover = calculate_enzyme_turnover( enzyme_pool=enzyme_pool_size, turnover_rate=enzyme_turnover_rate ) @@ -1066,6 +1048,35 @@ def calculate_net_enzyme_change( return (enzyme_production - enzyme_turnover, enzyme_turnover) +def calculate_enzyme_production( + microbial_groups: dict[str, MicrobialGroupConstants], + growth_rates: dict[str, NDArray[np.float32]], +) -> dict[str, NDArray[np.float32]]: + """Calculate the total production of each enzyme class. + + This function checks which substrates each functional group produces enzymes for, + and then calculates the enzyme productions based on the growth rates and the + proportional enzyme production. + + Args: + microbial_groups: Set of microbial functional groups defined in the soil model + growth_rates: The (gross) growth rates of each microbial group [kg C m^-3 + day^-1] + + Returns: + A dictionary containing the total production rate of each enzyme class [kg C + m^-3 day^-1] + """ + + return { + f"{group.name}_{substrate}": ( + growth_rates[group.name] * group.enzyme_production[substrate] + ) + for group in microbial_groups.values() + for substrate in group.find_enzyme_substrates() + } + + def calculate_maintenance_biomass_synthesis( microbe_pool_size: NDArray[np.float32], soil_temp: NDArray[np.float32], @@ -1163,6 +1174,13 @@ def calculate_nutrient_uptake_rates( remain the same if nutrients are taken up following the same stoichiometry (with an adjustment made for carbon use efficiency). + Biomass synthesis is split between the synthesis of new cellular biomass and the + production of extracellular enzymes. We assume that extracellular enzymes are always + produced in fixed proportion to the rate at which new biomass is synthesised. As + such, we calculate the nutrient costs of synthesising new biomass based on a + weighted (by relative investment in production) average of the stoichiometry of the + different enzymes and the microbial group itself. + The balance of mineralisation and immobilisation rates of inorganic nitrogen and phosphorus are also calculated in this function. This is done by calculating the difference between the demand for nitrogen and phosphorus and their uptake due to @@ -1193,9 +1211,9 @@ def calculate_nutrient_uptake_rates( functional group Returns: - A tuple containing the rate at which microbial biomass increases due to nutrient - uptake, as well as a dataclass containing the rate at which carbon, nitrogen and - phosphorus get taken up. + A tuple containing the rate at which microbial (cellular) biomass increases due + to nutrient uptake, as well as a dataclass containing the rate at which carbon, + nitrogen and phosphorus get taken up. """ # Calculate highest possible microbial uptake rates for organic matter and inorganic @@ -1271,13 +1289,13 @@ def calculate_nutrient_uptake_rates( actual_carbon_gain = np.minimum.reduce( [ carbon_gain_max, - functional_group.c_n_ratio + functional_group.synthesis_nutrient_ratios["nitrogen"] * ( organic_nitrogen_uptake_rate_max + ammonium_uptake_rate_max + nitrate_uptake_rate_max ), - functional_group.c_p_ratio + functional_group.synthesis_nutrient_ratios["phosphorus"] * ( organic_phosphorus_uptake_rate_max + inorganic_phosphorus_uptake_rate_max @@ -1292,7 +1310,9 @@ def calculate_nutrient_uptake_rates( # Calculate uptake/release of inorganic nitrogen based on difference between # stoichiometric demand and organic nitrogen uptake - nitrogen_demand = actual_carbon_gain / functional_group.c_n_ratio + nitrogen_demand = ( + actual_carbon_gain / functional_group.synthesis_nutrient_ratios["nitrogen"] + ) inorganic_nitrogen_change = nitrogen_demand - actual_organic_nitrogen_uptake # For immobilisation of nitrogen, the proportion of ammonium and nitrate taken up @@ -1315,7 +1335,9 @@ def calculate_nutrient_uptake_rates( # Calculate uptake/release of inorganic phosphorus based on difference between # stoichiometric demand and organic phosphorus uptake - phosphorus_demand = actual_carbon_gain / functional_group.c_p_ratio + phosphorus_demand = ( + actual_carbon_gain / functional_group.synthesis_nutrient_ratios["phosphorus"] + ) inorganic_phosphorus_change = phosphorus_demand - actual_organic_phosphorus_uptake consumption_rates = NetNutrientConsumption( @@ -1331,7 +1353,9 @@ def calculate_nutrient_uptake_rates( # respired instead of being uptaken. This isn't currently of interest, but will be # in future - return actual_carbon_gain, consumption_rates + return actual_carbon_gain / ( + 1 + sum(functional_group.enzyme_production.values()) + ), consumption_rates def calculate_highest_achievable_nutrient_uptake( @@ -1651,9 +1675,9 @@ def calculate_soil_nutrient_mineralisation( def calculate_nutrient_flows_to_necromass( bacterial_loss: NDArray[np.float32], fungal_loss: NDArray[np.float32], - bacterial_enzyme_denaturation: NDArray[np.float32], - fungal_enzyme_denaturation: NDArray[np.float32], + enzyme_changes: EnzymePoolChanges, microbial_groups: dict[str, MicrobialGroupConstants], + enzyme_classes: dict[str, EnzymeConstants], ) -> tuple[NDArray[np.float32], NDArray[np.float32]]: """Calculate the rate at which nutrients flow into the necromass pool. @@ -1664,26 +1688,39 @@ def calculate_nutrient_flows_to_necromass( bacterial_loss: Rate at which bacterial biomass becomes necromass [kg C m^-3 day^-1] fungal_loss: Rate at which fungal biomass becomes necromass [kg C m^-3 day^-1] - bacterial_enzyme_denaturation: Rate at which enzymes produced by bacteria - denature [kg C m^-3 day^-1] - fungal_enzyme_denaturation: Rate at which enzymes produced by fungi denature [kg - C m^-3 day^-1] + enzyme_changes: Details of the rate change for the soil enzyme pools. microbial_groups: Set of microbial functional groups defined in the soil model + enzyme_classes: Details of the enzyme classes used by the soil model. Returns: A tuple containing the rates at which nitrogen [kg N m^-3 day^-1] and phosphorus [kg P m^-3 day^-1] are added to the soil necromass pool """ + # Calculate nutrient flows due to cellular losses + necromass_n_cellular = (bacterial_loss / microbial_groups["bacteria"].c_n_ratio) + ( + fungal_loss / microbial_groups["fungi"].c_n_ratio + ) + necromass_p_cellular = (bacterial_loss / microbial_groups["bacteria"].c_p_ratio) + ( + fungal_loss / microbial_groups["fungi"].c_p_ratio + ) + + necromass_n_enzyme = sum( + getattr(enzyme_changes, f"denaturation_{substrate}_{group}") + / enzyme_classes[f"{group}_{substrate}"].c_n_ratio + for group in ["bacteria", "fungi"] + for substrate in ["maom", "pom"] + ) + necromass_p_enzyme = sum( + getattr(enzyme_changes, f"denaturation_{substrate}_{group}") + / enzyme_classes[f"{group}_{substrate}"].c_p_ratio + for group in ["bacteria", "fungi"] + for substrate in ["maom", "pom"] + ) + return ( - (bacterial_loss / microbial_groups["bacteria"].c_n_ratio) - + (fungal_loss / microbial_groups["fungi"].c_n_ratio) - + (bacterial_enzyme_denaturation / microbial_groups["bacteria"].c_n_ratio) - + (fungal_enzyme_denaturation / microbial_groups["fungi"].c_n_ratio), - (bacterial_loss / microbial_groups["bacteria"].c_p_ratio) - + (fungal_loss / microbial_groups["fungi"].c_p_ratio) - + (bacterial_enzyme_denaturation / microbial_groups["bacteria"].c_p_ratio) - + (fungal_enzyme_denaturation / microbial_groups["fungi"].c_p_ratio), + necromass_n_cellular + necromass_n_enzyme, + necromass_p_cellular + necromass_p_enzyme, ) diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index c9f310ecf..edc5e207c 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -199,8 +199,10 @@ def from_config( "Information required to initialise the soil model successfully extracted." ) - microbial_groups = make_full_set_of_microbial_groups(config) enzyme_classes = make_full_set_of_enzymes(config) + microbial_groups = make_full_set_of_microbial_groups( + config, enzyme_classes=enzyme_classes + ) return cls( data=data,