diff --git a/docs/source/_toc.yaml b/docs/source/_toc.yaml index 5b6042ae2..d8bf36bca 100644 --- a/docs/source/_toc.yaml +++ b/docs/source/_toc.yaml @@ -202,6 +202,8 @@ subtrees: title: The constants submodule - file: api/models/soil/env_factors title: The env_factors submodule + - file: api/models/soil/microbial_groups + title: The microbial_groups submodule - file: api/models/soil/soil_model title: The soil_model submodule - file: api/models/plants diff --git a/docs/source/api/models/soil/microbial_groups.md b/docs/source/api/models/soil/microbial_groups.md new file mode 100644 index 000000000..855891c8f --- /dev/null +++ b/docs/source/api/models/soil/microbial_groups.md @@ -0,0 +1,33 @@ +--- +jupytext: + cell_metadata_filter: -all + formats: md:myst + main_language: python + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.7 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +language_info: + codemirror_mode: + name: ipython + version: 3 + file_extension: .py + mimetype: text/x-python + name: python + nbconvert_exporter: python + pygments_lexer: ipython3 + version: 3.11.9 +--- + +# API documentation for the {mod}`~virtual_ecosystem.models.soil.microbial_groups` module + +```{eval-rst} +.. automodule:: virtual_ecosystem.models.soil.microbial_groups + :autosummary: + :members: +``` diff --git a/tests/conftest.py b/tests/conftest.py index 6b62e40c2..c5644d188 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,7 +83,47 @@ def reset_module_registry(): @pytest.fixture -def fixture_config(): +def microbial_groups_cfg(): + """Configuration string containing full set of required microbial groups.""" + return """ + [[soil.microbial_group_definition]] + name = "bacteria" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + + [[soil.microbial_group_definition]] + name = "fungi" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 6.5 + c_p_ratio = 40.0 + """ + + +@pytest.fixture +def fixture_config(microbial_groups_cfg): """Simple configuration fixture for use in tests.""" from virtual_ecosystem.core.config import Config @@ -250,7 +290,7 @@ def fixture_config(): [hydrology] """ - return Config(cfg_strings=cfg_string) + return Config(cfg_strings=[cfg_string, microbial_groups_cfg]) @pytest.fixture @@ -288,7 +328,8 @@ def dummy_carbon_data(fixture_core_components): data_values = { "soil_c_pool_lmwc": [0.05, 0.02, 0.1, 0.005], "soil_c_pool_maom": [2.5, 1.7, 4.5, 0.5], - "soil_c_pool_microbe": [5.8, 2.3, 11.3, 1.0], + "soil_c_pool_bacteria": [5.8, 2.3, 11.3, 1.0], + "soil_c_pool_fungi": [0.89, 8.55, 2.21, 4.54], "soil_c_pool_pom": [0.1, 1.0, 0.7, 0.35], "soil_c_pool_necromass": [0.058, 0.015, 0.093, 0.105], "soil_enzyme_pom": [0.022679, 0.009576, 0.050051, 0.003010], diff --git a/tests/core/test_data.py b/tests/core/test_data.py index 39b928c65..1a46eccb9 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -968,7 +968,8 @@ def test_output_current_state(mocker, dummy_carbon_data, time_index): [ "soil_c_pool_maom", "soil_c_pool_lmwc", - "soil_c_pool_microbe", + "soil_c_pool_bacteria", + "soil_c_pool_fungi", "soil_c_pool_pom", "soil_c_pool_necromass", "soil_enzyme_pom", diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index d6e7a2ca4..8e22328db 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -6,13 +6,15 @@ @pytest.fixture -def fixture_soil_config(): +def fixture_soil_config(microbial_groups_cfg): """Create a soil config with faster update interval.""" from virtual_ecosystem.core.config import Config return Config( - cfg_strings="[core]\n[core.timing]\nupdate_interval = '12 hours'\n[soil]\n" - "[hydrology]\n" + cfg_strings=[ + "[core]\n[core.timing]\nupdate_interval = '12 hours'", + microbial_groups_cfg, + ] ) @@ -120,7 +122,7 @@ def microbial_changes( soil_n_pool_nitrate=dummy_carbon_data["soil_n_pool_nitrate"], soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"], - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + soil_c_pool_bacteria=dummy_carbon_data["soil_c_pool_bacteria"], soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"], soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"], soil_temp=dummy_carbon_data["soil_temperature"][ @@ -177,3 +179,13 @@ def maom_desorption(dummy_carbon_data): soil_c_pool_maom=dummy_carbon_data["soil_c_pool_maom"], desorption_rate_constant=SoilConsts.maom_desorption_rate, ) + + +@pytest.fixture +def functional_groups(fixture_config): + """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) diff --git a/tests/models/soil/test_microbial_groups.py b/tests/models/soil/test_microbial_groups.py new file mode 100644 index 000000000..80b4638a3 --- /dev/null +++ b/tests/models/soil/test_microbial_groups.py @@ -0,0 +1,197 @@ +"""Test module for soil.microbial_groups.py. + +This module tests the functions which generate microbial functional groups. +""" + +from logging import CRITICAL + +import pytest + +from tests.conftest import log_check +from virtual_ecosystem.core.config import Config, ConfigurationError + + +def test_make_full_set_of_microbial_groups(fixture_config): + """Test that the function to make all the microbial group works.""" + from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroupConstants, + make_full_set_of_microbial_groups, + ) + + expected_groups = ["bacteria", "fungi"] + + functional_groups = make_full_set_of_microbial_groups(fixture_config) + + assert set(expected_groups) == set(functional_groups.keys()) + + for group in expected_groups: + assert type(functional_groups[group]) is MicrobialGroupConstants + + # Only testing one value, as testing them all seems like overkill/hard to maintain + assert functional_groups["bacteria"].c_n_ratio == 5.2 + assert functional_groups["fungi"].c_n_ratio == 6.5 + + +@pytest.mark.parametrize( + argnames=["cfg_strings", "exp_log"], + argvalues=[ + pytest.param( + """[core]""", + [ + (CRITICAL, "Model configuration for soil model not found."), + ], + id="no_soil_config", + ), + pytest.param( + """ + [[soil.microbial_group_definition]] + name = "bacteria" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + """, + [ + ( + CRITICAL, + "The following expected soil microbial groups are not defined: " + "fungi", + ) + ], + id="missing_fungi", + ), + pytest.param( # archaea included but they shouldn't be + """ + [[soil.microbial_group_definition]] + name = "bacteria" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + + [[soil.microbial_group_definition]] + name = "fungi" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + + [[soil.microbial_group_definition]] + name = "archaea" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + """, + [ + ( + CRITICAL, + "The following microbial groups are not valid: archaea", + ), + ], + id="unexpected_archaea", + ), + pytest.param( + """ + [[soil.microbial_group_definition]] + name = "bacteria" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + + [[soil.microbial_group_definition]] + name = "archaea" + max_uptake_rate_labile_C = 0.04 + activation_energy_uptake_rate = 47000 + half_sat_labile_C_uptake = 0.364 + activation_energy_uptake_saturation = 30000 + max_uptake_rate_ammonium = 5e-3 + half_sat_ammonium_uptake = 0.02275 + max_uptake_rate_nitrate = 5e-4 + half_sat_nitrate_uptake = 0.02275 + max_uptake_rate_labile_p = 0.0025 + half_sat_labile_p_uptake = 0.02275 + turnover_rate = 0.005 + activation_energy_turnover = 20000 + c_n_ratio = 5.2 + c_p_ratio = 16 + """, + [ + ( + CRITICAL, + "The following expected soil microbial groups are not defined: " + "fungi", + ), + ( + CRITICAL, + "The following microbial groups are not valid: archaea", + ), + ], + id="missing_fungi_and_unexpected_archaea", + ), + ], +) +def test_make_full_set_of_microbial_groups_errors(caplog, 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, + ) + + config = Config(cfg_strings=cfg_strings) + caplog.clear() + + with pytest.raises(ConfigurationError): + _ = make_full_set_of_microbial_groups(config) + + log_check(caplog, exp_log) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 63b488453..1754d215d 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -9,7 +9,9 @@ from virtual_ecosystem.models.soil.constants import SoilConsts -def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): +def test_calculate_all_pool_updates( + dummy_carbon_data, fixture_core_components, functional_groups +): """Test that the two pool update functions work correctly.""" from virtual_ecosystem.core.constants import CoreConsts from virtual_ecosystem.models.soil.pools import SoilPools @@ -38,30 +40,32 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): data=dummy_carbon_data, pools=pools, constants=SoilConsts, + functional_groups=functional_groups, max_depth_of_microbial_activity=CoreConsts.max_depth_of_microbial_activity, ) change_in_pools = { - "soil_c_pool_lmwc": [0.114984117633, 0.0533384581, 0.23449812333, 0.03425546], + "soil_c_pool_lmwc": [0.114909863, 0.0426977357, 0.23275147271, 0.033993336945], "soil_c_pool_maom": [0.038767651, 0.00829848, 0.05982197, 0.07277182], - "soil_c_pool_microbe": [-0.054361097, -0.022606231, -0.118911406, -0.007195167], + "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.00177803841, -0.007860960795, -0.012016245, 0.00545032], - "soil_c_pool_necromass": [0.001137474, 0.009172067, 0.033573266, -0.08978050], - "soil_enzyme_pom": [1.18e-8, 1.67e-8, 1.8e-9, -1.12e-8], - "soil_enzyme_maom": [-0.00031009, -5.09593e-5, 0.0005990658, -3.72112e-5], - "soil_n_pool_don": [0.00120201, 0.004654495, 0.005055088, 0.002542567], + "soil_c_pool_necromass": [0.00932274, 0.09290406, 0.05659641, -0.05764445], + "soil_enzyme_pom": [8.3534893e-5, 0.0008544245, 0.0002349318, 0.0003279076], + "soil_enzyme_maom": [-0.000226569, 0.0008034485, 0.0008339958, 0.0002907076], + "soil_n_pool_don": [0.00120116138, 0.00389444416, 0.00505259291, 0.00239278244], "soil_n_pool_particulate": [1.102338e-5, 6.422491e-5, 0.000131687, 1.461799e-5], - "soil_n_pool_necromass": [0.00786114, -0.01209909, 0.00432363, -0.00891218], + "soil_n_pool_necromass": [0.00912041, 0.000782751, 0.007865652, -0.00396817], "soil_n_pool_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_n_pool_ammonium": [0.000752008, 0.019813667, 0.000465414, 5.5603e-5], - "soil_n_pool_nitrate": [-0.003293386, -0.004012927, -0.001035765, -0.000655954], - "soil_p_pool_dop": [0.000194453, 7.1014337e-5, 0.0001851685, 0.0001017010], + "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": [0.0001944445, 5.8853523e-5, 0.0001841704, 9.5709618e-5], "soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7], - "soil_p_pool_necromass": [2.674836e-3, 1.333056e-3, 6.8090685e-3, 4.1429847e-5], + "soil_p_pool_necromass": [0.002879471, 0.003426353, 0.007384646, 0.000844827], "soil_p_pool_maom": [5.52086672e-4, 3.68566732e-5, 4.7566130e-4, 3.09257058e-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.577278e-5, -0.0002194777, -8.060241e-5, -4.159191e-6], + "soil_p_pool_labile": [-1.643259e-5, -0.000295103, -9.270421e-5, -1.313285e-6], } # Make order of pools object @@ -84,7 +88,7 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): assert np.allclose(delta_pools[i * 4 : (i + 1) * 4], change_in_pools[pool]) -def test_to_per_volume(dummy_carbon_data, fixture_core_components): +def test_to_per_volume(dummy_carbon_data, fixture_core_components, functional_groups): """Test that the SoilPools.to_per_volume method converts correctly.""" from virtual_ecosystem.core.constants import CoreConsts from virtual_ecosystem.models.soil.pools import SoilPools @@ -113,6 +117,7 @@ def test_to_per_volume(dummy_carbon_data, fixture_core_components): data=dummy_carbon_data, pools=pools, constants=SoilConsts, + functional_groups=functional_groups, max_depth_of_microbial_activity=CoreConsts.max_depth_of_microbial_activity, ) @@ -125,23 +130,26 @@ def test_to_per_volume(dummy_carbon_data, fixture_core_components): def test_calculate_microbial_changes( - dummy_carbon_data, fixture_core_components, environmental_factors + dummy_carbon_data, fixture_core_components, environmental_factors, functional_groups ): """Check that calculation of microbe related changes works correctly.""" from virtual_ecosystem.models.soil.pools import calculate_microbial_changes expected_mic_changes = { - "lmwc_uptake": [0.000193562715, 0.00114496662, 0.00403724667, 5.77363558e-5], - "don_uptake": [2.2121431e-6, 8.17832483e-5, 5.76720686e-6, 3.29921934e-5], - "ammonium_change": [2.57532644e-6, -8.2097145e-6, 0.00019762123, -2.4896415e-5], - "nitrate_change": [8.61302611e-6, -9.1219050e-7, 2.9529644e-5, -2.7662684e-6], - "dop_uptake": [2.2120347e-8, 1.30853197e-6, 2.3069958e-6, 1.3196877e-6], - "labile_p_change": [4.333041e-6, 2.230641e-5, 7.339138e-5, 4.124029e-7], - "microbe_change": [-0.054361097, -0.022606231, -0.118911406, -0.007195167], - "pom_enzyme_change": [1.17571917e-8, 1.6744223e-8, 1.8331136e-9, -1.1167587e-8], - "maom_enzyme_change": [-3.1009224e-4, -5.0959256e-5, 5.990658e-4, -3.721117e-5], - "necromass_generation": [0.05474086, 0.02303502, 0.11952352, 0.00726011], + "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_change": [8.3534893e-5, 0.0008544245, 0.0002349318, 0.0003279076], + "maom_enzyme_change": [-0.000226569, 0.0008034485, 0.0008339958, 0.0002907076], + "necromass_generation": [0.062926123, 0.106766979, 0.142546653, 0.03939614], + "necromass_n_flow": [0.0117863, 0.0173117, 0.0265273, 0.0063402], + "necromass_p_flow": [0.003625935, 0.003532987, 0.008045798, 0.001257157], } actual_mic_changes = calculate_microbial_changes( @@ -151,7 +159,8 @@ def test_calculate_microbial_changes( soil_n_pool_nitrate=dummy_carbon_data["soil_n_pool_nitrate"], soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"], - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + soil_c_pool_bacteria=dummy_carbon_data["soil_c_pool_bacteria"], + soil_c_pool_fungi=dummy_carbon_data["soil_c_pool_fungi"], soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"], soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"], soil_temp=dummy_carbon_data["soil_temperature"][ @@ -159,6 +168,7 @@ def test_calculate_microbial_changes( ], env_factors=environmental_factors, constants=SoilConsts, + functional_groups=functional_groups, ) for attr in dir(actual_mic_changes): @@ -272,16 +282,20 @@ def test_calculate_enzyme_changes(dummy_carbon_data): from virtual_ecosystem.models.soil.pools import calculate_enzyme_changes - biomass_loss = np.array([0.05443078, 0.02298407, 0.12012258, 0.00722288]) + bacterial_biomass_loss = np.array([0.05443078, 0.02298407, 0.12012258, 0.00722288]) + fungal_biomass_loss = np.array( + [0.00835230934, 0.08544078195, 0.02349300015, 0.0327918752] + ) - expected_pom = [1.17571917e-8, 1.67442231e-8, 1.83311362e-9, -1.11675865e-8] - expected_maom = [-3.10092243e-4, -5.09592558e-5, 5.99065833e-4, -3.72111676e-5] + expected_pom = [8.35348934e-5, 8.54424519e-4, 2.34931801e-4, 3.27907552e-4] + expected_maom = [-0.00022656911, 0.000803448520, 0.000833995802, 0.000290707552] expected_denat = [0.0013987, 0.00051062, 0.00180338, 0.00018168] actual_pom, actual_maom, actual_denat = calculate_enzyme_changes( soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"], soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"], - biomass_loss=biomass_loss, + bacterial_biomass_loss=bacterial_biomass_loss, + fungal_biomass_loss=fungal_biomass_loss, constants=SoilConsts, ) @@ -291,7 +305,7 @@ def test_calculate_enzyme_changes(dummy_carbon_data): def test_calculate_maintenance_biomass_synthesis( - dummy_carbon_data, fixture_core_components + dummy_carbon_data, fixture_core_components, functional_groups ): """Check maintenance respiration cost calculates correctly.""" from virtual_ecosystem.models.soil.pools import ( @@ -301,11 +315,12 @@ def test_calculate_maintenance_biomass_synthesis( expected_loss = [0.05443078, 0.02298407, 0.12012258, 0.00722288] actual_loss = calculate_maintenance_biomass_synthesis( - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + microbe_pool_size=dummy_carbon_data["soil_c_pool_bacteria"], soil_temp=dummy_carbon_data["soil_temperature"][ fixture_core_components.layer_structure.index_topsoil_scalar ], - constants=SoilConsts, + microbial_group=functional_groups["bacteria"], + reference_temperature=SoilConsts.arrhenius_reference_temp, ) assert np.allclose(actual_loss, expected_loss) @@ -358,7 +373,7 @@ def test_calculate_enzyme_turnover(dummy_carbon_data, turnover, expected_decay): def test_calculate_nutrient_uptake_rates( - dummy_carbon_data, fixture_core_components, environmental_factors + dummy_carbon_data, fixture_core_components, environmental_factors, functional_groups ): """Check microbial carbon uptake calculates correctly.""" from virtual_ecosystem.models.soil.pools import ( @@ -382,13 +397,14 @@ def test_calculate_nutrient_uptake_rates( soil_n_pool_nitrate=dummy_carbon_data["soil_n_pool_nitrate"], soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"], - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + microbial_pool_size=dummy_carbon_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 ].to_numpy(), constants=SoilConsts, + functional_group=functional_groups["bacteria"], ) assert np.allclose(actual_carbon_gain, expected_carbon_gain) @@ -405,7 +421,7 @@ def test_calculate_nutrient_uptake_rates( def test_calculate_highest_achievable_nutrient_uptake( - dummy_carbon_data, fixture_core_components, environmental_factors + dummy_carbon_data, fixture_core_components, environmental_factors, functional_groups ): """Check function to calculate maximum possible uptake rates works as intended.""" from virtual_ecosystem.models.soil.pools import ( @@ -416,22 +432,28 @@ def test_calculate_highest_achievable_nutrient_uptake( actual_uptake = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=dummy_carbon_data["soil_c_pool_lmwc"], - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + microbial_pool_size=dummy_carbon_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 ].to_numpy(), - max_uptake_rate=SoilConsts.max_uptake_rate_labile_C, - half_saturation_constant=SoilConsts.half_sat_labile_C_uptake, - constants=SoilConsts, + max_uptake_rate=functional_groups["bacteria"].max_uptake_rate_labile_C, + activation_energy_uptake=functional_groups[ + "bacteria" + ].activation_energy_uptake_rate, + half_saturation_constant=functional_groups["bacteria"].half_sat_labile_C_uptake, + activation_energy_uptake_saturation=functional_groups[ + "bacteria" + ].activation_energy_uptake_saturation, + reference_temperature=SoilConsts.arrhenius_reference_temp, ) assert np.allclose(actual_uptake, expected_uptake) def test_negative_highest_achievable_nutrient_uptake_are_impossible( - dummy_carbon_data, fixture_core_components, environmental_factors + dummy_carbon_data, fixture_core_components, environmental_factors, functional_groups ): """Test to check that negative maximum uptake rates cannot be returned.""" from virtual_ecosystem.models.soil.pools import ( @@ -446,15 +468,21 @@ def test_negative_highest_achievable_nutrient_uptake_are_impossible( actual_uptake = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=labile_carbon_data, - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + microbial_pool_size=dummy_carbon_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 ].to_numpy(), - max_uptake_rate=SoilConsts.max_uptake_rate_labile_C, - half_saturation_constant=SoilConsts.half_sat_labile_C_uptake, - constants=SoilConsts, + max_uptake_rate=functional_groups["bacteria"].max_uptake_rate_labile_C, + activation_energy_uptake=functional_groups[ + "bacteria" + ].activation_energy_uptake_rate, + half_saturation_constant=functional_groups["bacteria"].half_sat_labile_C_uptake, + activation_energy_uptake_saturation=functional_groups[ + "bacteria" + ].activation_energy_uptake_saturation, + reference_temperature=SoilConsts.arrhenius_reference_temp, ) assert np.allclose(actual_uptake, expected_uptake) @@ -621,18 +649,29 @@ def test_calculate_soil_nutrient_mineralisation( assert np.allclose(actual_rate, expected_rate) -def test_calculate_nutrient_flows_to_necromass(microbial_changes): +def test_calculate_nutrient_flows_to_necromass(functional_groups): """Test that the function to calculate nutrient flows to necromass works.""" from virtual_ecosystem.models.soil.pools import ( calculate_nutrient_flows_to_necromass, ) - expected_n_flow_to_necromass = [0.01052709, 0.00442981, 0.02298529, 0.00139617] - expected_p_flow_to_necromass = [0.0034213, 0.00143969, 0.00747022, 0.00045376] + bacterial_biomass_loss = np.array( + [0.0533421644, 0.0225243886, 0.1177201284, 0.0070784224] + ) + fungal_biomass_loss = np.array( + [0.008185263158, 0.083731966317, 0.023023140156, 0.032136037696] + ) + enzyme_denaturation = np.array([0.0013987, 0.00051062, 0.00180338, 0.00018168]) + + expected_n_flow_to_necromass = [0.0117863, 0.0173117, 0.0265273, 0.0063402] + expected_p_flow_to_necromass = [0.003625935, 0.003532987, 0.008045798, 0.001257157] actual_n_flow_to_necromass, actual_p_flow_to_necromass = ( calculate_nutrient_flows_to_necromass( - microbial_changes=microbial_changes, constants=SoilConsts + bacterial_loss=bacterial_biomass_loss, + fungal_loss=fungal_biomass_loss, + enzyme_denaturation=enzyme_denaturation, + microbial_groups=functional_groups, ) ) diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 2b44e66d5..7df777a49 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -16,7 +16,8 @@ REQUIRED_INIT_VAR_LOG = ( (DEBUG, "soil model: required var 'soil_c_pool_maom' checked"), (DEBUG, "soil model: required var 'soil_c_pool_lmwc' checked"), - (DEBUG, "soil model: required var 'soil_c_pool_microbe' checked"), + (DEBUG, "soil model: required var 'soil_c_pool_bacteria' checked"), + (DEBUG, "soil model: required var 'soil_c_pool_fungi' checked"), (DEBUG, "soil model: required var 'soil_c_pool_pom' checked"), (DEBUG, "soil model: required var 'soil_c_pool_necromass' checked"), (DEBUG, "soil model: required var 'soil_enzyme_pom' checked"), @@ -47,7 +48,7 @@ def test_soil_model_initialization( - caplog, dummy_carbon_data, fixture_soil_core_components + caplog, dummy_carbon_data, fixture_soil_core_components, functional_groups ): """Test `SoilModel` initialization with good data.""" from virtual_ecosystem.core.base_model import BaseModel @@ -59,6 +60,7 @@ def test_soil_model_initialization( data=dummy_carbon_data, core_components=fixture_soil_core_components, model_constants=SoilConsts(), + microbial_groups=functional_groups, soil_moisture_capacity=CoreConsts.soil_moisture_capacity, ) @@ -121,7 +123,7 @@ def test_soil_model_initialization_no_data( def test_soil_model_initialization_bounds_error( - caplog, dummy_carbon_data, fixture_core_components + caplog, dummy_carbon_data, fixture_core_components, functional_groups ): """Test `SoilModel` initialization.""" from virtual_ecosystem.core.constants import CoreConsts @@ -139,6 +141,7 @@ def test_soil_model_initialization_bounds_error( data=dummy_carbon_data, core_components=fixture_core_components, model_constants=SoilConsts(), + microbial_groups=functional_groups, soil_moisture_capacity=CoreConsts.soil_moisture_capacity, ) @@ -153,7 +156,9 @@ def test_soil_model_initialization_bounds_error( ) -def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_components): +def test_soil_model_all_pools_positive( + dummy_carbon_data, fixture_core_components, functional_groups +): """Test `SoilModel` initialization.""" from virtual_ecosystem.core.constants import CoreConsts from virtual_ecosystem.models.soil.constants import SoilConsts @@ -164,6 +169,7 @@ def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_component data=dummy_carbon_data, core_components=fixture_core_components, model_constants=SoilConsts(), + microbial_groups=functional_groups, soil_moisture_capacity=CoreConsts.soil_moisture_capacity, ) @@ -181,7 +187,7 @@ def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_component "cfg_string,max_decomp,raises,expected_log_entries", [ pytest.param( - "[core]\n[core.timing]\nupdate_interval = '12 hours'\n[soil]", + "", 60.0, does_not_raise(), ( @@ -196,7 +202,6 @@ def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_component id="default_config", ), pytest.param( - "[core]\n[core.timing]\nupdate_interval = '12 hours'\n" "[soil.constants.SoilConsts]\nmax_decomp_rate_pom = 0.05", 0.05, does_not_raise(), @@ -212,7 +217,6 @@ def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_component id="modified_config_correct", ), pytest.param( - "[core]\n[core.timing]\nupdate_interval = '12 hours'\n" "[soil.constants.SoilConsts]\nmax_decomp_rate = 0.05\n", None, pytest.raises(ConfigurationError), @@ -228,6 +232,7 @@ def test_soil_model_all_pools_positive(dummy_carbon_data, fixture_core_component def test_generate_soil_model( caplog, dummy_carbon_data, + microbial_groups_cfg, cfg_string, max_decomp, raises, @@ -244,7 +249,13 @@ def test_generate_soil_model( register_module("virtual_ecosystem.models.soil") # Build the config object and core components - config = Config(cfg_strings=cfg_string) + config = Config( + cfg_strings=[ + "[core]\n[core.timing]\nupdate_interval = '12 hours'", + microbial_groups_cfg, + cfg_string, + ] + ) core_components = CoreComponents(config) caplog.clear() @@ -323,60 +334,62 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.10714899, 0.04687765, 0.21697935, 0.01985447], dims="cell_id" + [0.10716745, 0.04243441, 0.21608526, 0.02032034], dims="cell_id" ), soil_c_pool_maom=DataArray( - [2.5194618, 1.70483236, 4.53238116, 0.52968038], dims="cell_id" + [2.52007289, 1.71105702, 4.5340965, 0.53207841], dims="cell_id" ), - soil_c_pool_microbe=DataArray( - [5.77301132, 2.28872611, 11.24101287, 0.99642308], + soil_c_pool_bacteria=DataArray( + [5.77302395, 2.28877945, 11.24105325, 0.99642196], + dims="cell_id", + ), + soil_c_pool_fungi=DataArray( + [0.88589732, 8.50953322, 2.19873017, 4.52379322], dims="cell_id", ), soil_c_pool_pom=DataArray( - [0.10088826, 0.99607827, 0.69401858, 0.35272508], dims="cell_id" + [0.10088811, 0.99597975, 0.69401136, 0.35272452], dims="cell_id" ), soil_c_pool_necromass=DataArray( - [0.05840102, 0.01864856, 0.10631116, 0.06904722], dims="cell_id" + [0.06167055, 0.05209188, 0.11550502, 0.0818911], dims="cell_id" ), soil_enzyme_pom=DataArray( - [0.02267842, 0.00957576, 0.05004963, 0.00300993], dims="cell_id" + [0.02271979, 0.00999937, 0.0501659, 0.00317262], dims="cell_id" ), soil_enzyme_maom=DataArray( - [0.0354453, 0.01167442, 0.02538637, 0.00454144], dims="cell_id" + [0.03548666, 0.01209803, 0.02550264, 0.00470413], dims="cell_id" ), soil_n_pool_don=DataArray( - [0.00136286, 0.00345323, 0.00275385, 0.00390608], dims="cell_id" + [0.00139199, 0.00327139, 0.00282787, 0.00394078], dims="cell_id" ), soil_n_pool_particulate=DataArray( - [0.00714836, 0.00074629, 0.00292269, 0.01429302], dims="cell_id" + [0.00714835, 0.00074622, 0.00292266, 0.014293], dims="cell_id" ), soil_n_pool_necromass=DataArray( - [0.00602168, 0.01303568, 0.02189821, 0.00758444], dims="cell_id" + [0.0065247, 0.01818108, 0.02331271, 0.00956047], dims="cell_id" ), soil_n_pool_maom=DataArray( - [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" + [0.86680802, 0.4867186, 0.33433055, 0.09972284], dims="cell_id" ), soil_n_pool_ammonium=DataArray( - [0.00043285, 0.01492311, 0.00041098, 0.00518662], - dims="cell_id", + [0.00042711, 0.01507407, 0.0003601, 0.00524337], dims="cell_id" ), soil_n_pool_nitrate=DataArray( - [0.00056445, 0.00201929, -0.00015952, 0.01270619], + [0.00056236, 0.00203603, -0.00016227, 0.01271297], dims="cell_id", ), soil_p_pool_dop=DataArray( - [1.68943459e-4, 9.12472720e-5, 3.17089050e-4, 1.66121801e-4], - dims="cell_id", + [0.00017381, 0.000129, 0.00032976, 0.00018233], dims="cell_id" ), soil_p_pool_particulate=DataArray( - [3.21780215e-5, 2.85147941e-4, 1.14676885e-4, 5.71721209e-4], + [3.21779733e-5, 2.85119757e-4, 1.14675695e-4, 5.71720292e-4], dims="cell_id", ), soil_p_pool_necromass=DataArray( - [0.00187527, 0.00064763, 0.00343346, 0.00046239], dims="cell_id" + [0.00195702, 0.00148389, 0.00366335, 0.00078355], dims="cell_id" ), soil_p_pool_maom=DataArray( - [0.01355237, 0.03473323, 0.01997613, 0.00400384], dims="cell_id" + [0.01356763, 0.03488897, 0.02001905, 0.0040638], dims="cell_id" ), soil_p_pool_primary=DataArray( [0.0019594, 0.00535662, 0.00277434, 0.00059892], dims="cell_id" @@ -385,7 +398,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.00705643, 0.03816757, 0.01152552, 0.00733096], dims="cell_id" ), soil_p_pool_labile=DataArray( - [-4.83245627e-6, -7.7008184e-5, 1.03175587e-5, 1.91604386e-4], + [-6.3944329e-6, -1.18639819e-4, 9.86215495e-7, 1.91352432e-4], dims="cell_id", ), ) @@ -545,7 +558,9 @@ def test_calculate_dissolved_nutrient_concentrations_negative(fixture_soil_model assert np.allclose(actual_concs[nutrient], expected_concs[nutrient]) -def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): +def test_construct_full_soil_model( + dummy_carbon_data, fixture_core_components, functional_groups +): """Test that the function that creates the object to integrate exists and works.""" from virtual_ecosystem.core.constants import CoreConsts from virtual_ecosystem.models.soil.constants import SoilConsts @@ -555,10 +570,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): ) delta_pools = [ - 0.114984117633, - 0.0533384581, - 0.23449812333, - 0.03425546, + 0.114909863, + 0.0426977357, + 0.23275147271, + 0.033993336945, 0.038767651, 0.00829848, 0.05982197, @@ -567,58 +582,62 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -0.022606231, -0.118911406, -0.007195167, + -0.0083255777, + -0.0819293436, + -0.022969005, + -0.032666056, 0.00177803841, -0.007860960795, -0.012016245, 0.00545032, - 0.001137474, - 0.009172067, - 0.033573266, - -0.08978050, - 1.17571917e-8, - 1.67442231e-8, - 1.83311362e-9, - -1.11675865e-08, - -0.00031009, - -5.09593e-5, - 0.0005990658, - -3.72112e-5, - 0.00120201, - 0.004654495, - 0.005055088, - 0.002542567, + 0.00932274, + 0.09290406, + 0.05659641, + -0.05764445, + 8.3534893e-5, + 0.0008544245, + 0.0002349318, + 0.0003279076, + -0.000226569, + 0.0008034485, + 0.0008339958, + 0.0002907076, + 0.00120116138, + 0.00389444416, + 0.00505259291, + 0.00239278244, 1.102338e-5, 6.422491e-5, 0.000131687, 1.461799e-5, - 0.00786114, - -0.01209909, - 0.00432363, - -0.00891218, + 0.00912041, + 0.000782751, + 0.007865652, + -0.00396817, 0.00148604, 0.01179891, 0.01365197, 0.0077315, - 0.000752008, - 0.019813667, - 0.000465414, - 5.5603e-5, - -0.003293386, - -0.004012927, - -0.001035765, - -0.000655954, - 0.000194453, - 7.1014337e-5, - 0.0001851685, - 0.0001017010, + 0.00075125671, + 0.02001151359, + 0.00039745, + 0.000172988, + -0.003295899, + -0.003990944, + -0.001045921, + -0.000642911, + 0.0001944445, + 5.8853523e-5, + 0.0001841704, + 9.5709618e-5, 7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7, - 2.674836e-3, - 1.333056e-3, - 6.8090685e-3, - 4.1429847e-5, + 0.002879471, + 0.003426353, + 0.007384646, + 0.000844827, 5.52086672e-4, 3.68566732e-5, 4.7566130e-4, @@ -631,10 +650,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -2.77311e-6, -7.40324e-7, -2.187697e-7, - -1.577278e-5, - -0.0002194777, - -8.060241e-5, - -4.159191e-6, + -1.643259e-5, + -0.000295103, + -9.270421e-5, + -1.313285e-6, ] # make pools @@ -661,6 +680,7 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): top_soil_layer_index=fixture_core_components.layer_structure.index_topsoil_scalar, delta_pools_ordered=delta_pools_ordered, model_constants=SoilConsts, + functional_groups=functional_groups, max_depth_of_microbial_activity=CoreConsts.max_depth_of_microbial_activity, soil_moisture_capacity=CoreConsts.soil_moisture_capacity, top_soil_layer_thickness=fixture_core_components.layer_structure.soil_layer_thickness[ diff --git a/tests/test_main.py b/tests/test_main.py index 7663a1152..1985927ba 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -23,7 +23,8 @@ ), (DEBUG, "soil model: required var 'soil_c_pool_maom' checked"), (DEBUG, "soil model: required var 'soil_c_pool_lmwc' checked"), - (DEBUG, "soil model: required var 'soil_c_pool_microbe' checked"), + (DEBUG, "soil model: required var 'soil_c_pool_bacteria' checked"), + (DEBUG, "soil model: required var 'soil_c_pool_fungi' checked"), (DEBUG, "soil model: required var 'soil_c_pool_pom' checked"), (DEBUG, "soil model: required var 'soil_c_pool_necromass' checked"), (DEBUG, "soil model: required var 'soil_enzyme_pom' checked"), @@ -103,6 +104,7 @@ def test_initialise_models( caplog, dummy_carbon_data, + microbial_groups_cfg, cfg_strings, output, raises, @@ -116,7 +118,7 @@ def test_initialise_models( # Generate a configuration to use, using simple inputs to populate most from # defaults. Then clear the caplog to isolate the logging for the function, - config = Config(cfg_strings=cfg_strings) + config = Config(cfg_strings=[cfg_strings, microbial_groups_cfg]) core_components = CoreComponents(config) caplog.clear() diff --git a/virtual_ecosystem/data_variables.toml b/virtual_ecosystem/data_variables.toml index 552f9094d..fd79a1cfd 100644 --- a/virtual_ecosystem/data_variables.toml +++ b/virtual_ecosystem/data_variables.toml @@ -553,8 +553,15 @@ variable_type = "float" [[variable]] axis = ["spatial"] -description = "Soil microbial biomass (carbon) pool" -name = "soil_c_pool_microbe" +description = "Soil bacterial biomass (carbon) pool" +name = "soil_c_pool_bacteria" +unit = "kg C m^-3" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Soil fungal biomass (carbon) pool" +name = "soil_c_pool_fungi" unit = "kg C m^-3" variable_type = "float" diff --git a/virtual_ecosystem/example_data/config/data_config.toml b/virtual_ecosystem/example_data/config/data_config.toml index f055ce1af..89c19c264 100644 --- a/virtual_ecosystem/example_data/config/data_config.toml +++ b/virtual_ecosystem/example_data/config/data_config.toml @@ -52,7 +52,10 @@ file_path = "../data/example_soil_data.nc" var_name = "soil_c_pool_maom" [[core.data.variable]] file_path = "../data/example_soil_data.nc" -var_name = "soil_c_pool_microbe" +var_name = "soil_c_pool_bacteria" +[[core.data.variable]] +file_path = "../data/example_soil_data.nc" +var_name = "soil_c_pool_fungi" [[core.data.variable]] file_path = "../data/example_soil_data.nc" var_name = "soil_c_pool_pom" diff --git a/virtual_ecosystem/example_data/config/soil_microbial_groups.toml b/virtual_ecosystem/example_data/config/soil_microbial_groups.toml new file mode 100644 index 000000000..1f6d41e39 --- /dev/null +++ b/virtual_ecosystem/example_data/config/soil_microbial_groups.toml @@ -0,0 +1,33 @@ +[[soil.microbial_group_definition]] +name = "bacteria" +max_uptake_rate_labile_C = 0.04 +activation_energy_uptake_rate = 47000 +half_sat_labile_C_uptake = 0.364 +activation_energy_uptake_saturation = 30000 +max_uptake_rate_ammonium = 5e-3 +half_sat_ammonium_uptake = 0.02275 +max_uptake_rate_nitrate = 5e-4 +half_sat_nitrate_uptake = 0.02275 +max_uptake_rate_labile_p = 0.0025 +half_sat_labile_p_uptake = 0.02275 +turnover_rate = 0.005 +activation_energy_turnover = 20000 +c_n_ratio = 5.2 +c_p_ratio = 16 + +[[soil.microbial_group_definition]] +name = "fungi" +max_uptake_rate_labile_C = 0.04 +activation_energy_uptake_rate = 47000 +half_sat_labile_C_uptake = 0.364 +activation_energy_uptake_saturation = 30000 +max_uptake_rate_ammonium = 5e-3 +half_sat_ammonium_uptake = 0.02275 +max_uptake_rate_nitrate = 5e-4 +half_sat_nitrate_uptake = 0.02275 +max_uptake_rate_labile_p = 0.0025 +half_sat_labile_p_uptake = 0.02275 +turnover_rate = 0.005 +activation_energy_turnover = 20000 +c_n_ratio = 6.5 +c_p_ratio = 40.0 \ No newline at end of file diff --git a/virtual_ecosystem/example_data/data/example_soil_data.nc b/virtual_ecosystem/example_data/data/example_soil_data.nc index 5109d789c..d6a0707d6 100644 Binary files a/virtual_ecosystem/example_data/data/example_soil_data.nc and b/virtual_ecosystem/example_data/data/example_soil_data.nc differ diff --git a/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py b/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py index 32b8ee431..6bb2321d9 100644 --- a/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py +++ b/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py @@ -28,9 +28,13 @@ # Generate a range of plausible values (1.0-3.0) for the maom pool [kg C m^-3]. maom_values = 1.0 + 2.0 * gradient / 64.0 -# Generate a range of plausible values (0.0015-0.005) for the microbial C pool +# Generate a range of plausible values (0.0015-0.005) for the bacterial C pool # [kg C m^-3]. -microbial_C_values = 0.0015 + 0.0035 * gradient / 64.0 +bacterial_C_values = 0.0015 + 0.0035 * gradient / 64.0 + +# Generate a range of plausible values (0.0015-0.005) for the fungal C pool +# [kg C m^-3]. +fungal_C_values = 0.0015 + 0.0035 * gradient / 64.0 # Generate a range of plausible values (0.1-1.0) for the POM pool [kg C m^-3]. pom_values = 0.1 + 0.9 * gradient / 64.0 @@ -100,7 +104,8 @@ clay_fraction=(["x", "y"], clay_fraction_values), soil_c_pool_lmwc=(["x", "y"], lmwc_values), soil_c_pool_maom=(["x", "y"], maom_values), - soil_c_pool_microbe=(["x", "y"], microbial_C_values), + soil_c_pool_bacteria=(["x", "y"], bacterial_C_values), + soil_c_pool_fungi=(["x", "y"], fungal_C_values), soil_c_pool_pom=(["x", "y"], pom_values), soil_c_pool_necromass=(["x", "y"], necromass_values), soil_enzyme_pom=(["x", "y"], pom_enzyme_values), diff --git a/virtual_ecosystem/models/soil/__init__.py b/virtual_ecosystem/models/soil/__init__.py index 732439124..b1e06f82b 100644 --- a/virtual_ecosystem/models/soil/__init__.py +++ b/virtual_ecosystem/models/soil/__init__.py @@ -6,10 +6,12 @@ * The :mod:`~virtual_ecosystem.models.soil.soil_model` submodule instantiates the SoilModel class which consolidates the functionality of the soil module into a single class, which the high level functions of the Virtual Ecosystem can then make use of. -* The :mod:`~virtual_ecosystem.models.soil.pools` provides functionality to track - all soil pools over time. +* The :mod:`~virtual_ecosystem.models.soil.pools` provides functionality to track all + soil pools over time. * The :mod:`~virtual_ecosystem.models.soil.env_factors` provides functions that capture the impact of environmental factors on microbial rates. +* The :mod:`~virtual_ecosystem.models.soil.microbial_groups` provides the microbial + functional groups used in the soil model. * The :mod:`~virtual_ecosystem.models.soil.constants` provides a set of dataclasses containing the constants required by the broader soil model. """ # noqa: D205 diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 885b86fc2..fe1cc8678 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -57,44 +57,6 @@ class SoilConsts(ConstantsDataclass): the source of the activation energies and corresponding rates. """ - # TODO - Split this and the following into 2 constants once fungi are introduced - max_uptake_rate_labile_C: float = 0.04 - """Maximum rate at the reference temperature of labile carbon uptake [day^-1]. - - The reference temperature is given - by :attr:`arrhenius_reference_temp`, and the corresponding activation energy is - given by :attr:`activation_energy_microbial_uptake`. - - TODO - Source of this constant is not completely clear, investigate this further - once fungi are added. - """ - - activation_energy_microbial_uptake: float = 47000 - """Activation energy for microbial nutrient uptake [J K^-1]. - - Value taken from :cite:t:`wang_development_2013`. The maximum labile carbon uptake - rate that this activation energy corresponds to is given by - :attr:`max_uptake_rate_labile_C`. This activation energy is assumed to be the same - for the uptake of other nutrients as for carbon. - """ - - half_sat_labile_C_uptake: float = 0.364 - """Half saturation constant for microbial uptake of labile carbon (LMWC). - - [kg C m^-3]. This was calculated from the value provided in - :cite:t:`wang_development_2013` assuming an average bulk density of 1400 [kg m^-3]. - The reference temperature is given by :attr:`arrhenius_reference_temp`, and the - corresponding activation energy is given by - :attr:`activation_energy_uptake_saturation`. - """ - - activation_energy_uptake_saturation: float = 30000 - """Activation energy for nutrient uptake saturation constants [J K^-1]. - - Taken from :cite:t:`wang_development_2013`. This is assumed to be the same across - all nutrients. - """ - half_sat_pom_decomposition: float = 70.0 """Half saturation constant for POM decomposition to LMWC [kg C m^-3]. @@ -157,25 +119,6 @@ class SoilConsts(ConstantsDataclass): Units of [J K^-1]. Taken from :cite:t:`wang_development_2013`. """ - # TODO - Split this and the following into 2 constants once fungi are introduced - microbial_turnover_rate: float = 0.005 - """Microbial turnover rate at reference temperature [day^-1]. - - The reference temperature is given by :attr:`arrhenius_reference_temp`, and the - corresponding activation energy is given by - :attr:`activation_energy_microbial_turnover`. - - TODO - Source of this constant is not completely clear, investigate this further - once fungi are added. - """ - - activation_energy_microbial_turnover = 20000 - """Activation energy for microbial maintenance turnover rate [J K^-1]. - - Value taken from :cite:t:`wang_development_2013`. The microbial turnover rate that - this activation energy corresponds to is given by :attr:`microbial_turnover_rate`. - """ - # TODO - At some point I need to split these enzyme constants into fungi and # bacteria specific constants pom_enzyme_turnover_rate: float = 2.4e-2 @@ -190,20 +133,32 @@ class SoilConsts(ConstantsDataclass): Value taken from :cite:t:`wang_development_2013`. """ - maintenance_pom_enzyme: float = 1e-2 - """Fraction of maintenance synthesis used to produce POM degrading enzymes. + bacterial_maintenance_pom_enzyme: float = 1e-2 + """Fraction of bacterial maintenance used to produce POM degrading enzymes. + + [unitless]. Value taken from :cite:t:`wang_development_2013`. + """ + + fungal_maintenance_pom_enzyme: float = 1e-2 + """Fraction of fungal maintenance used to produce POM degrading enzymes. + + [unitless]. Value taken from :cite:t:`wang_development_2013`. + """ + + bacterial_maintenance_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_maom_enzyme: float = 1e-2 - """Fraction of maintenance synthesis used to produce MAOM degrading enzymes. + fungal_maintenance_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 - The 4 constants below should take different values for fungi and bacteria, - # once that separation is implemented. + # TODO - At some point, need to allow microbial and fungal environmental factors to + # vary min_pH_microbes: float = 2.5 """Soil pH below which microbial activity is completely inhibited [unitless]. @@ -347,22 +302,6 @@ class SoilConsts(ConstantsDataclass): leaches from litter solely in organic form. """ - microbial_c_n_ratio: float = 5.2 - """Ratio of carbon to nitrogen in microbial biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. Here using specifically the bacterial value, once fungi are - added this constant needs to be split. - """ - - microbial_c_p_ratio: float = 16 - """Ratio of carbon to phosphorus in microbial biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. Here using specifically the bacterial value, once fungi are - added this constant needs to be split. - """ - ammonium_mineralisation_proportion: float = 0.9 """Proportion of microbially mineralised nitrogen that takes the form of ammonium. @@ -371,72 +310,6 @@ class SoilConsts(ConstantsDataclass): particularly clear. """ - max_uptake_rate_ammonium: float = 5e-3 - """Maximum possible rate for ammonium uptake [day^-1]. - - This rate corresponds to the reference temperature given by - :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by - :attr:`activation_energy_microbial_uptake`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - - half_sat_ammonium_uptake: float = 0.02275 - """Half saturation constant for uptake of ammonium [kg N m^-3]. - - The reference temperature is given by :attr:`arrhenius_reference_temp`, and the - corresponding activation energy is given by - :attr:`activation_energy_uptake_saturation`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - - max_uptake_rate_nitrate: float = 5e-4 - """Maximum possible rate for nitrate uptake [day^-1]. - - This rate corresponds to the reference temperature given by - :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by - :attr:`activation_energy_microbial_uptake`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - - half_sat_nitrate_uptake: float = 0.02275 - """Half saturation constant for uptake of nitrate [kg N m^-3]. - - The reference temperature is given by :attr:`arrhenius_reference_temp`, and the - corresponding activation energy is given by - :attr:`activation_energy_uptake_saturation`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - - max_uptake_rate_labile_p: float = 0.0025 - """Maximum possible rate for labile inorganic phosphorus uptake [day^-1]. - - This rate corresponds to the reference temperature given by - :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by - :attr:`activation_energy_microbial_uptake`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - - half_sat_labile_p_uptake: float = 0.02275 - """Half saturation constant for uptake of labile inorganic phosphorus. - - [kg P m^-3]. The reference temperature is given by :attr:`arrhenius_reference_temp`, - and the corresponding activation energy is given by - :attr:`activation_energy_uptake_saturation`. - - TODO - At present I've invented the value for this constant, so it really needs to - be better pinned down. - """ - tectonic_uplift_rate_phosphorus: float = 0.0 """Rate at which tectonic uplift exposes new primary phosphorus [kg P m^-3 day^-1]. diff --git a/virtual_ecosystem/models/soil/microbial_groups.py b/virtual_ecosystem/models/soil/microbial_groups.py new file mode 100644 index 000000000..db6b9523b --- /dev/null +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -0,0 +1,123 @@ +"""The ``models.soil.microbial_groups`` module contains the classes needed to define +the different microbial functional groups used in the soil model. +""" # noqa: D205 + +from dataclasses import dataclass + +from virtual_ecosystem.core.config import Config, ConfigurationError +from virtual_ecosystem.core.logger import LOGGER + + +@dataclass(frozen=True) +class MicrobialGroupConstants: + """Container for the set of constants associated with a microbial functional group. + + This sets out the constants which must be defined for each microbial functional + group. + """ + + name: str + """The name of the microbial group functional type.""" + + max_uptake_rate_labile_C: float + """Maximum rate at the reference temperature of labile carbon uptake [day^-1].""" + + activation_energy_uptake_rate: float + """Activation energy for nutrient uptake [J K^-1].""" + + half_sat_labile_C_uptake: float + """Half saturation constant for uptake of labile carbon (LMWC) [kg C m^-3].""" + + activation_energy_uptake_saturation: float + """Activation energy for nutrient uptake saturation constants [J K^-1].""" + + max_uptake_rate_ammonium: float + """Maximum possible rate for ammonium uptake [day^-1].""" + + half_sat_ammonium_uptake: float + """Half saturation constant for uptake of ammonium [kg N m^-3].""" + + max_uptake_rate_nitrate: float + """Maximum possible rate for nitrate uptake [day^-1].""" + + half_sat_nitrate_uptake: float + """Half saturation constant for uptake of nitrate [kg N m^-3].""" + + max_uptake_rate_labile_p: float + """Maximum possible rate for labile inorganic phosphorus uptake [day^-1].""" + + half_sat_labile_p_uptake: float + """Half saturation constant for uptake of labile inorganic phosphorus [kg P m^-3]. + """ + + turnover_rate: float + """Microbial maintenance turnover rate at reference temperature [day^-1].""" + + activation_energy_turnover: float + """Activation energy for microbial maintenance turnover rate [J K^-1].""" + + c_n_ratio: float + """Ratio of carbon to nitrogen in biomass [unitless].""" + + c_p_ratio: float + """Ratio of carbon to phosphorus in biomass [unitless].""" + + +def make_full_set_of_microbial_groups( + config: Config, +) -> dict[str, MicrobialGroupConstants]: + """Make the full set of functional groups used in the soil model. + + Args: + config: The complete virtual ecosystem config. + + Raises: + ConfigurationError: If the soil model configuration is missing, if expected + functional groups are not defined, or if unexpected functional groups are + defined. + + Returns: + A dictionary containing each functional group used in the soil model (currently + bacteria and fungi). + """ + + if "soil" not in config: + msg = "Model configuration for soil model not found." + LOGGER.critical(msg) + raise ConfigurationError(msg) + + expected_groups = {"fungi", "bacteria"} + defined_groups = { + group["name"] for group in config["soil"]["microbial_group_definition"] + } + + undefined_groups = expected_groups.difference(defined_groups) + unexpected_groups = defined_groups.difference(expected_groups) + if undefined_groups: + msg = ( + "The following expected soil microbial groups are not defined: " + f"{', '.join(set(expected_groups) - set(defined_groups))}" + ) + LOGGER.critical(msg) + if unexpected_groups: + msg = ( + "The following microbial groups are not valid: " + f"{', '.join(set(defined_groups) - set(expected_groups))}" + ) + LOGGER.critical(msg) + if undefined_groups or unexpected_groups: + raise ConfigurationError( + "The soil microbial group configuration contains errors. Please check the " + "log." + ) + + return { + group_name: MicrobialGroupConstants( + **next( + functional_group + for functional_group in config["soil"]["microbial_group_definition"] + if functional_group["name"] == group_name + ) + ) + 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 b2cb1035c..1aed01467 100644 --- a/virtual_ecosystem/models/soil/module_schema.json +++ b/virtual_ecosystem/models/soil/module_schema.json @@ -17,9 +17,80 @@ "SoilConsts" ] }, + "microbial_group_definition": { + "description": "An microbial functional group definitions", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "max_uptake_rate_labile_C": { + "type": "number" + }, + "activation_energy_uptake_rate": { + "type": "number" + }, + "half_sat_labile_C_uptake": { + "type": "number" + }, + "activation_energy_uptake_saturation": { + "type": "number" + }, + "max_uptake_rate_ammonium": { + "type": "number" + }, + "half_sat_ammonium_uptake": { + "type": "number" + }, + "max_uptake_rate_nitrate": { + "type": "number" + }, + "half_sat_nitrate_uptake": { + "type": "number" + }, + "max_uptake_rate_labile_p": { + "type": "number" + }, + "half_sat_labile_p_uptake": { + "type": "number" + }, + "turnover_rate": { + "type": "number" + }, + "activation_energy_turnover": { + "type": "number" + }, + "c_n_ratio": { + "type": "number" + }, + "c_p_ratio": { + "type": "number" + } + }, + "required": [ + "name", + "max_uptake_rate_labile_C", + "activation_energy_uptake_rate", + "half_sat_labile_C_uptake", + "activation_energy_uptake_saturation", + "max_uptake_rate_ammonium", + "half_sat_ammonium_uptake", + "max_uptake_rate_nitrate", + "half_sat_nitrate_uptake", + "max_uptake_rate_labile_p", + "half_sat_labile_p_uptake", + "turnover_rate", + "activation_energy_turnover", + "c_n_ratio", + "c_p_ratio" + ] + } + }, "static": { - "type": "boolean", - "default": false + "type": "boolean", + "default": false } }, "default": {}, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index bbd6d4fd6..0be36ab29 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -24,6 +24,7 @@ calculate_symbiotic_nitrogen_fixation_carbon_cost, calculate_temperature_effect_on_microbes, ) +from virtual_ecosystem.models.soil.microbial_groups import MicrobialGroupConstants # TODO - At this point in time I'm not adding specific phosphatase enzymes, need to # think about adding these in future @@ -67,8 +68,11 @@ class MicrobialChanges: and mineralisation of labile P. A positive value indicates a net immobilisation (uptake) of P. """ - microbe_change: NDArray[np.float32] - """Rate of change of microbial biomass pool [kg C m^-3 day^-1].""" + bacteria_change: NDArray[np.float32] + """Rate of change of bacterial biomass pool [kg C m^-3 day^-1].""" + + fungi_change: NDArray[np.float32] + """Rate of change of fungal biomass pool [kg C m^-3 day^-1].""" pom_enzyme_change: NDArray[np.float32] """Rate of change of particulate organic matter degrading enzyme pool. @@ -85,6 +89,12 @@ class MicrobialChanges: necromass_generation: NDArray[np.float32] """Rate at which necromass is being produced [kg C m^-3 day^-1].""" + necromass_n_flow: NDArray[np.float32] + """Nitrogen flow associated with necromass generation [kg N m^-3 day^-1].""" + + necromass_p_flow: NDArray[np.float32] + """Phosphorus flow associated with necromass generation [kg P m^-3 day^-1].""" + @dataclass class NetNutrientConsumption: @@ -195,8 +205,11 @@ class PoolData: soil_c_pool_lmwc: NDArray[np.float32] """Low molecular weight carbon pool [kg C m^-3].""" - soil_c_pool_microbe: NDArray[np.float32] - """Microbial biomass pool [kg C m^-3].""" + soil_c_pool_bacteria: NDArray[np.float32] + """Bacterial biomass pool [kg C m^-3].""" + + soil_c_pool_fungi: NDArray[np.float32] + """Fungal biomass pool [kg C m^-3].""" soil_c_pool_pom: NDArray[np.float32] """Particulate organic matter pool [kg C m^-3].""" @@ -276,6 +289,7 @@ def __init__( data: Data, pools: dict[str, NDArray[np.float32]], constants: SoilConsts, + functional_groups: dict[str, MicrobialGroupConstants], max_depth_of_microbial_activity: float, ): self.data = data @@ -290,6 +304,9 @@ def __init__( self.constants = constants """Set of constants for the soil model.""" + self.functional_groups = functional_groups + """Set of microbial functional groups used by the soil model.""" + self.max_depth_of_microbial_activity = max_depth_of_microbial_activity """Maximum depth of the soil profile where microbial activity occurs [m].""" @@ -359,12 +376,14 @@ def calculate_all_pool_updates( soil_n_pool_nitrate=self.pools.soil_n_pool_nitrate, soil_p_pool_dop=self.pools.soil_p_pool_dop, soil_p_pool_labile=self.pools.soil_p_pool_labile, - soil_c_pool_microbe=self.pools.soil_c_pool_microbe, + soil_c_pool_bacteria=self.pools.soil_c_pool_bacteria, + soil_c_pool_fungi=self.pools.soil_c_pool_fungi, soil_enzyme_pom=self.pools.soil_enzyme_pom, soil_enzyme_maom=self.pools.soil_enzyme_maom, soil_temp=soil_temperature, env_factors=env_factors, constants=self.constants, + functional_groups=self.functional_groups, ) # find changes driven by the enzyme pools enzyme_mediated = calculate_enzyme_mediated_rates( @@ -436,10 +455,6 @@ def calculate_all_pool_updates( breakdown_rate=enzyme_mediated.pom_to_lmwc, ) - # Find flow of nitrogen to necromass pool - necromass_n_flow, necromass_p_flow = calculate_nutrient_flows_to_necromass( - microbial_changes=microbial_changes, constants=self.constants - ) # Find nitrogen released by necromass breakdown/sorption necromass_outflows = find_necromass_nutrient_outflows( necromass_carbon=self.pools.soil_c_pool_necromass, @@ -534,8 +549,8 @@ def calculate_all_pool_updates( - enzyme_mediated.maom_to_lmwc - maom_desorption_to_lmwc ) - - delta_pools_ordered["soil_c_pool_microbe"] = microbial_changes.microbe_change + delta_pools_ordered["soil_c_pool_bacteria"] = microbial_changes.bacteria_change + delta_pools_ordered["soil_c_pool_fungi"] = microbial_changes.fungi_change delta_pools_ordered["soil_c_pool_pom"] = ( litter_mineralisation_flux.pom - enzyme_mediated.pom_to_lmwc ) @@ -558,7 +573,7 @@ def calculate_all_pool_updates( litter_mineralisation_flux.particulate_n - pom_n_mineralisation ) delta_pools_ordered["soil_n_pool_necromass"] = ( - necromass_n_flow + microbial_changes.necromass_n_flow - necromass_outflows["decay_nitrogen"] - necromass_outflows["sorption_nitrogen"] ) @@ -596,7 +611,7 @@ def calculate_all_pool_updates( litter_mineralisation_flux.particulate_p - pom_p_mineralisation ) delta_pools_ordered["soil_p_pool_necromass"] = ( - necromass_p_flow + microbial_changes.necromass_p_flow - necromass_outflows["decay_phosphorus"] - necromass_outflows["sorption_phosphorus"] ) @@ -645,12 +660,14 @@ def calculate_microbial_changes( soil_n_pool_nitrate: NDArray[np.float32], soil_p_pool_dop: NDArray[np.float32], soil_p_pool_labile: NDArray[np.float32], - soil_c_pool_microbe: NDArray[np.float32], + soil_c_pool_bacteria: NDArray[np.float32], + soil_c_pool_fungi: NDArray[np.float32], soil_enzyme_pom: NDArray[np.float32], soil_enzyme_maom: NDArray[np.float32], soil_temp: NDArray[np.float32], env_factors: EnvironmentalEffectFactors, constants: SoilConsts, + functional_groups: dict[str, MicrobialGroupConstants], ) -> MicrobialChanges: """Calculate the changes for the microbial biomass and enzyme pools. @@ -666,7 +683,8 @@ def calculate_microbial_changes( soil_n_pool_nitrate: Soil nitrate pool [kg N m^-3] soil_p_pool_dop: Dissolved organic phosphorus pool [kg P m^-3] soil_p_pool_labile: Labile inorganic phosphorus pool [kg P m^-3] - soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] + soil_c_pool_bacteria: Bacterial biomass (carbon) pool [kg C m^-3] + soil_c_pool_fungi: Fungal biomass (carbon) pool [kg C m^-3] soil_enzyme_pom: Amount of enzyme class which breaks down particulate organic matter [kg C m^-3] soil_enzyme_maom: Amount of enzyme class which breaks down mineral associated @@ -675,6 +693,7 @@ def calculate_microbial_changes( env_factors: Data class containing the various factors through which the environment effects soil cycling rates. constants: Set of constants for the soil model. + functional_groups: Set of microbial functional groups used by the soil model. Returns: A dataclass containing the rate at which microbes uptake LMWC, DON and DOP, and @@ -682,50 +701,97 @@ def calculate_microbial_changes( """ # Calculate uptake, growth rate, and loss rate - biomass_growth, microbial_uptake = calculate_nutrient_uptake_rates( + bacterial_growth, bacterial_uptake = calculate_nutrient_uptake_rates( soil_c_pool_lmwc=soil_c_pool_lmwc, soil_n_pool_don=soil_n_pool_don, soil_n_pool_ammonium=soil_n_pool_ammonium, soil_n_pool_nitrate=soil_n_pool_nitrate, soil_p_pool_dop=soil_p_pool_dop, soil_p_pool_labile=soil_p_pool_labile, - soil_c_pool_microbe=soil_c_pool_microbe, + microbial_pool_size=soil_c_pool_bacteria, water_factor=env_factors.water, pH_factor=env_factors.pH, soil_temp=soil_temp, constants=constants, + functional_group=functional_groups["bacteria"], ) - biomass_loss = calculate_maintenance_biomass_synthesis( - soil_c_pool_microbe=soil_c_pool_microbe, + bacterial_biomass_loss = calculate_maintenance_biomass_synthesis( + microbe_pool_size=soil_c_pool_bacteria, + soil_temp=soil_temp, + microbial_group=functional_groups["bacteria"], + reference_temperature=constants.arrhenius_reference_temp, + ) + fungal_growth, fungal_uptake = calculate_nutrient_uptake_rates( + soil_c_pool_lmwc=soil_c_pool_lmwc, + soil_n_pool_don=soil_n_pool_don, + soil_n_pool_ammonium=soil_n_pool_ammonium, + soil_n_pool_nitrate=soil_n_pool_nitrate, + soil_p_pool_dop=soil_p_pool_dop, + soil_p_pool_labile=soil_p_pool_labile, + microbial_pool_size=soil_c_pool_fungi, + water_factor=env_factors.water, + pH_factor=env_factors.pH, soil_temp=soil_temp, constants=constants, + functional_group=functional_groups["fungi"], + ) + fungal_biomass_loss = calculate_maintenance_biomass_synthesis( + microbe_pool_size=soil_c_pool_fungi, + soil_temp=soil_temp, + microbial_group=functional_groups["fungi"], + reference_temperature=constants.arrhenius_reference_temp, ) # Find changes in each enzyme pool pom_enzyme_net_change, maom_enzyme_net_change, enzyme_denaturation = ( calculate_enzyme_changes( soil_enzyme_pom=soil_enzyme_pom, soil_enzyme_maom=soil_enzyme_maom, - biomass_loss=biomass_loss, + bacterial_biomass_loss=bacterial_biomass_loss, + fungal_biomass_loss=fungal_biomass_loss, constants=constants, ) ) # Find fraction of loss that isn't enzyme production - true_loss = ( - 1 - constants.maintenance_pom_enzyme - constants.maintenance_maom_enzyme - ) * biomass_loss + true_bacterial_loss = ( + 1 + - constants.bacterial_maintenance_pom_enzyme + - constants.bacterial_maintenance_maom_enzyme + ) * bacterial_biomass_loss + true_fungal_loss = ( + 1 + - constants.fungal_maintenance_pom_enzyme + - constants.fungal_maintenance_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, + enzyme_denaturation=enzyme_denaturation, + microbial_groups=functional_groups, + ) return MicrobialChanges( - lmwc_uptake=microbial_uptake.carbon, - don_uptake=microbial_uptake.organic_nitrogen, - ammonium_change=microbial_uptake.ammonium, - nitrate_change=microbial_uptake.nitrate, - dop_uptake=microbial_uptake.organic_phosphorus, - labile_p_change=microbial_uptake.inorganic_phosphorus, - microbe_change=biomass_growth - biomass_loss, + lmwc_uptake=bacterial_uptake.carbon + fungal_uptake.carbon, + don_uptake=bacterial_uptake.organic_nitrogen + fungal_uptake.organic_nitrogen, + ammonium_change=bacterial_uptake.ammonium + fungal_uptake.ammonium, + nitrate_change=bacterial_uptake.nitrate + fungal_uptake.nitrate, + dop_uptake=( + bacterial_uptake.organic_phosphorus + fungal_uptake.organic_phosphorus + ), + labile_p_change=( + bacterial_uptake.inorganic_phosphorus + fungal_uptake.inorganic_phosphorus + ), + bacteria_change=bacterial_growth - bacterial_biomass_loss, + fungi_change=fungal_growth - fungal_biomass_loss, pom_enzyme_change=pom_enzyme_net_change, maom_enzyme_change=maom_enzyme_net_change, - necromass_generation=enzyme_denaturation + true_loss, + necromass_generation=( + enzyme_denaturation + true_bacterial_loss + true_fungal_loss + ), + necromass_n_flow=necromass_n_flow, + necromass_p_flow=necromass_p_flow, ) @@ -866,7 +932,8 @@ def calculate_nutrient_leaching( def calculate_enzyme_changes( soil_enzyme_pom: NDArray[np.float32], soil_enzyme_maom: NDArray[np.float32], - biomass_loss: NDArray[np.float32], + bacterial_biomass_loss: NDArray[np.float32], + fungal_biomass_loss: NDArray[np.float32], constants: SoilConsts, ) -> tuple[NDArray[np.float32], NDArray[np.float32], NDArray[np.float32]]: """Calculate the changes to the concentration of each enzyme pool. @@ -880,9 +947,12 @@ def calculate_enzyme_changes( matter [kg C m^-3] soil_enzyme_maom: Amount of enzyme class which breaks down mineral associated organic matter [kg C m^-3] - biomass_loss: Rate a which the microbial biomass pool loses biomass, this is a - combination of enzyme excretion, protein degradation, and cell death [kg C - m^-3 day^-1] + bacterial_biomass_loss: Rate a which the bacterial biomass pool loses biomass, + this is a combination of enzyme excretion, protein degradation, and cell + death [kg C m^-3 day^-1] + fungal_biomass_loss: Rate a which the fungal biomass pool loses biomass, + this is a combination of enzyme excretion, protein degradation, and cell + death [kg C m^-3 day^-1] constants: Set of constants for the soil model. Returns: @@ -891,8 +961,14 @@ def calculate_enzyme_changes( """ # Calculate production an turnover of each enzyme class - pom_enzyme_production = constants.maintenance_pom_enzyme * biomass_loss - maom_enzyme_production = constants.maintenance_maom_enzyme * biomass_loss + pom_enzyme_production = ( + constants.bacterial_maintenance_pom_enzyme * bacterial_biomass_loss + + constants.fungal_maintenance_pom_enzyme * fungal_biomass_loss + ) + maom_enzyme_production = ( + constants.bacterial_maintenance_maom_enzyme * bacterial_biomass_loss + + constants.fungal_maintenance_maom_enzyme * fungal_biomass_loss + ) pom_enzyme_turnover = calculate_enzyme_turnover( enzyme_pool=soil_enzyme_pom, turnover_rate=constants.pom_enzyme_turnover_rate, @@ -911,20 +987,22 @@ def calculate_enzyme_changes( def calculate_maintenance_biomass_synthesis( - soil_c_pool_microbe: NDArray[np.float32], + microbe_pool_size: NDArray[np.float32], soil_temp: NDArray[np.float32], - constants: SoilConsts, + microbial_group: MicrobialGroupConstants, + reference_temperature: float, ) -> NDArray[np.float32]: - """Calculate microbial biomass synthesis rate required to offset losses. + """Calculate biomass synthesis rate required to offset losses for a microbial pool. In order for a microbial population to not decline it must synthesise enough new biomass to offset losses. These losses mostly come from cell death and protein decay, but also include loses due to extracellular enzyme excretion. Args: - soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] + microbe_pool_size: Size of the microbial pool of interest [kg C m^-3] soil_temp: soil temperature for each soil grid cell [degrees C] - constants: Set of constants for the soil model. + microbial_group: Constants associated with the microbial group of interest + reference_temperature: The reference temperature of the Arrhenius equation [C] Returns: The rate of microbial biomass loss that must be matched to maintain a steady @@ -933,11 +1011,11 @@ def calculate_maintenance_biomass_synthesis( temp_factor = calculate_temperature_effect_on_microbes( soil_temperature=soil_temp, - activation_energy=constants.activation_energy_microbial_turnover, - reference_temperature=constants.arrhenius_reference_temp, + activation_energy=microbial_group.activation_energy_turnover, + reference_temperature=reference_temperature, ) - return constants.microbial_turnover_rate * temp_factor * soil_c_pool_microbe + return microbial_group.turnover_rate * temp_factor * microbe_pool_size def calculate_carbon_use_efficiency( @@ -987,11 +1065,12 @@ def calculate_nutrient_uptake_rates( soil_n_pool_nitrate: NDArray[np.float32], soil_p_pool_dop: NDArray[np.float32], soil_p_pool_labile: NDArray[np.float32], - soil_c_pool_microbe: NDArray[np.float32], + microbial_pool_size: NDArray[np.float32], water_factor: NDArray[np.float32], pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], constants: SoilConsts, + functional_group: MicrobialGroupConstants, ) -> tuple[NDArray[np.float32], NetNutrientConsumption]: """Calculate the rate at which microbes uptake each nutrient. @@ -1025,13 +1104,15 @@ def calculate_nutrient_uptake_rates( soil_n_pool_nitrate: Soil nitrate pool [kg N m^-3] soil_p_pool_dop: Dissolved organic phosphorus pool [kg P m^-3] soil_p_pool_labile: Labile inorganic phosphorus pool [kg P m^-3] - soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] + microbial_pool_size: Amount of biomass for functional of interest [kg C m^-3] water_factor: A factor capturing the impact of soil water potential on microbial rates [unitless] pH_factor: A factor capturing the impact of soil pH on microbial rates [unitless] soil_temp: soil temperature for each soil grid cell [degrees C] constants: Set of constants for the soil model. + functional_group: A data class containing the parameters defining the microbial + functional group Returns: A tuple containing the rate at which microbial biomass increases due to nutrient @@ -1043,43 +1124,51 @@ def calculate_nutrient_uptake_rates( # forms of nitrogen and phosphorus carbon_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_c_pool_lmwc, - soil_c_pool_microbe=soil_c_pool_microbe, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_uptake_rate_labile_C, - half_saturation_constant=constants.half_sat_labile_C_uptake, - constants=constants, + max_uptake_rate=functional_group.max_uptake_rate_labile_C, + half_saturation_constant=functional_group.half_sat_labile_C_uptake, + activation_energy_uptake=functional_group.activation_energy_uptake_rate, + activation_energy_uptake_saturation=functional_group.activation_energy_uptake_saturation, + reference_temperature=constants.arrhenius_reference_temp, ) ammonium_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_n_pool_ammonium, - soil_c_pool_microbe=soil_c_pool_microbe, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_uptake_rate_ammonium, - half_saturation_constant=constants.half_sat_ammonium_uptake, - constants=constants, + max_uptake_rate=functional_group.max_uptake_rate_ammonium, + half_saturation_constant=functional_group.half_sat_ammonium_uptake, + activation_energy_uptake=functional_group.activation_energy_uptake_rate, + activation_energy_uptake_saturation=functional_group.activation_energy_uptake_saturation, + reference_temperature=constants.arrhenius_reference_temp, ) nitrate_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_n_pool_nitrate, - soil_c_pool_microbe=soil_c_pool_microbe, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_uptake_rate_nitrate, - half_saturation_constant=constants.half_sat_nitrate_uptake, - constants=constants, + max_uptake_rate=functional_group.max_uptake_rate_nitrate, + half_saturation_constant=functional_group.half_sat_nitrate_uptake, + activation_energy_uptake=functional_group.activation_energy_uptake_rate, + activation_energy_uptake_saturation=functional_group.activation_energy_uptake_saturation, + reference_temperature=constants.arrhenius_reference_temp, ) inorganic_phosphorus_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_p_pool_labile, - soil_c_pool_microbe=soil_c_pool_microbe, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_uptake_rate_labile_p, - half_saturation_constant=constants.half_sat_labile_p_uptake, - constants=constants, + max_uptake_rate=functional_group.max_uptake_rate_labile_p, + half_saturation_constant=functional_group.half_sat_labile_p_uptake, + activation_energy_uptake=functional_group.activation_energy_uptake_rate, + activation_energy_uptake_saturation=functional_group.activation_energy_uptake_saturation, + reference_temperature=constants.arrhenius_reference_temp, ) # Calculate carbon use efficiency and use to determine maximum possible rate of @@ -1104,13 +1193,13 @@ def calculate_nutrient_uptake_rates( actual_carbon_gain = np.minimum.reduce( [ carbon_gain_max, - constants.microbial_c_n_ratio + functional_group.c_n_ratio * ( organic_nitrogen_uptake_rate_max + ammonium_uptake_rate_max + nitrate_uptake_rate_max ), - constants.microbial_c_p_ratio + functional_group.c_p_ratio * ( organic_phosphorus_uptake_rate_max + inorganic_phosphorus_uptake_rate_max @@ -1125,7 +1214,7 @@ 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 / constants.microbial_c_n_ratio + nitrogen_demand = actual_carbon_gain / functional_group.c_n_ratio inorganic_nitrogen_change = nitrogen_demand - actual_organic_nitrogen_uptake # For immobilisation of nitrogen, the proportion of ammonium and nitrate taken up @@ -1148,7 +1237,7 @@ 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 / constants.microbial_c_p_ratio + phosphorus_demand = actual_carbon_gain / functional_group.c_p_ratio inorganic_phosphorus_change = phosphorus_demand - actual_organic_phosphorus_uptake consumption_rates = NetNutrientConsumption( @@ -1169,24 +1258,28 @@ def calculate_nutrient_uptake_rates( def calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool: NDArray[np.float32], - soil_c_pool_microbe: NDArray[np.float32], + microbial_pool_size: NDArray[np.float32], water_factor: NDArray[np.float32], pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], max_uptake_rate: float, + activation_energy_uptake: float, half_saturation_constant: float, - constants: SoilConsts, + activation_energy_uptake_saturation: float, + reference_temperature: float, ) -> NDArray[np.float32]: """Calculate highest achievable uptake rate for a specific nutrient. This function starts by calculating the impact that environmental factors have on the rate and saturation constants for microbial uptake. These constants are then - used to calculate the maximum possible uptake rate for the nutrient in question. + used to calculate the maximum possible uptake rate for the specific nutrient and + microbial group in question. Args: labile_nutrient_pool: Mass of nutrient that is in a readily uptakeable (labile) form [kg nut m^-3] - soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] + microbial_pool_size: Size of microbial biomass (carbon) pool of interest [kg C + m^-3] water_factor: A factor capturing the impact of soil water potential on microbial rates [unitless] pH_factor: A factor capturing the impact of soil pH on microbial rates @@ -1194,9 +1287,13 @@ def calculate_highest_achievable_nutrient_uptake( soil_temp: soil temperature for each soil grid cell [degrees C] max_uptake_rate: Maximum possible uptake rate of the nutrient (at reference temperature) [day^-1] + activation_energy_uptake: Activation energy for nutrient uptake for the + microbial group in question [J K^-1]. half_saturation_constant: Half saturation constant for nutrient uptake (at reference temperature) [kg nut m^-3] - constants: Set of constants for the soil model. + activation_energy_uptake_saturation: Activation energy for nutrient uptake + saturation for the microbial group in question [J K^-1]. + reference_temperature: The reference temperature of the Arrhenius equation [C] Returns: The maximum uptake rate by the soil microbial biomass for the nutrient in @@ -1206,13 +1303,13 @@ def calculate_highest_achievable_nutrient_uptake( # Calculate impact of temperature on the rate and saturation constants temp_factor_rate = calculate_temperature_effect_on_microbes( soil_temperature=soil_temp, - activation_energy=constants.activation_energy_microbial_uptake, - reference_temperature=constants.arrhenius_reference_temp, + activation_energy=activation_energy_uptake, + reference_temperature=reference_temperature, ) temp_factor_saturation = calculate_temperature_effect_on_microbes( soil_temperature=soil_temp, - activation_energy=constants.activation_energy_uptake_saturation, - reference_temperature=constants.arrhenius_reference_temp, + activation_energy=activation_energy_uptake_saturation, + reference_temperature=reference_temperature, ) # Rate and saturation constants are then adjusted based on these environmental # conditions @@ -1222,7 +1319,7 @@ def calculate_highest_achievable_nutrient_uptake( # Calculate both the rate of carbon uptake, and the rate at which this carbon is # assimilated into microbial biomass. uptake_rate = rate_constant * ( - (labile_nutrient_pool * soil_c_pool_microbe) + (labile_nutrient_pool * microbial_pool_size) / (labile_nutrient_pool + saturation_constant) ) @@ -1481,31 +1578,39 @@ def calculate_soil_nutrient_mineralisation( def calculate_nutrient_flows_to_necromass( - microbial_changes: MicrobialChanges, constants: SoilConsts + bacterial_loss: NDArray[np.float32], + fungal_loss: NDArray[np.float32], + enzyme_denaturation: NDArray[np.float32], + microbial_groups: dict[str, MicrobialGroupConstants], ) -> tuple[NDArray[np.float32], NDArray[np.float32]]: """Calculate the rate at which nutrients flow into the necromass pool. These flows comprise of the nitrogen and phosphorus content of the dead cells and denatured enzymes that flow into the necromass pool. - TODO - A core assumption here is that the stoichiometry of the enzymes are identical - to the microbial cells. This assumption works for now but will have to be revisited - when fungi are added (as they have different stoichiometric ratios but will - contribute to the same enzyme pools) - Args: - microbial_changes: Full set of changes to the microbial population due to - growth, death enzyme production, etc - constants: Set of constants for the soil model. + 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] + enzyme_denaturation: Rate at which enzymes denature [kg C m^-3 day^-1] + microbial_groups: Set of microbial functional groups defined in 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 """ + # TODO - Enzymes are assumed to have the same stoichiometry as bacteria, this is a + # placeholder assumption that we will lose when a more realistic enzyme production + # model is added (see issue #760) + return ( - microbial_changes.necromass_generation / constants.microbial_c_n_ratio, - microbial_changes.necromass_generation / constants.microbial_c_p_ratio, + (bacterial_loss / microbial_groups["bacteria"].c_n_ratio) + + (fungal_loss / microbial_groups["fungi"].c_n_ratio) + + (enzyme_denaturation / microbial_groups["bacteria"].c_n_ratio), + (bacterial_loss / microbial_groups["bacteria"].c_p_ratio) + + (fungal_loss / microbial_groups["fungi"].c_p_ratio) + + (enzyme_denaturation / microbial_groups["bacteria"].c_p_ratio), ) diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index 66abf8b18..56c343352 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -32,6 +32,10 @@ from virtual_ecosystem.core.exceptions import InitialisationError from virtual_ecosystem.core.logger import LOGGER from virtual_ecosystem.models.soil.constants import SoilConsts +from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroupConstants, + make_full_set_of_microbial_groups, +) from virtual_ecosystem.models.soil.pools import SoilPools @@ -46,7 +50,8 @@ class SoilModel( vars_required_for_init=( "soil_c_pool_maom", "soil_c_pool_lmwc", - "soil_c_pool_microbe", + "soil_c_pool_bacteria", + "soil_c_pool_fungi", "soil_c_pool_pom", "soil_c_pool_necromass", "soil_enzyme_pom", @@ -76,7 +81,8 @@ class SoilModel( vars_required_for_update=( "soil_c_pool_maom", "soil_c_pool_lmwc", - "soil_c_pool_microbe", + "soil_c_pool_bacteria", + "soil_c_pool_fungi", "soil_c_pool_pom", "soil_c_pool_necromass", "soil_enzyme_pom", @@ -110,7 +116,8 @@ class SoilModel( vars_updated=( "soil_c_pool_maom", "soil_c_pool_lmwc", - "soil_c_pool_microbe", + "soil_c_pool_bacteria", + "soil_c_pool_fungi", "soil_c_pool_pom", "soil_c_pool_necromass", "soil_enzyme_pom", @@ -184,16 +191,20 @@ def from_config( "Information required to initialise the soil model successfully extracted." ) + microbial_groups = make_full_set_of_microbial_groups(config) + return cls( data=data, core_components=core_components, static=static, model_constants=model_constants, + microbial_groups=microbial_groups, ) def _setup( self, model_constants: SoilConsts, + microbial_groups: dict[str, MicrobialGroupConstants], **kwargs: Any, ) -> None: """Function to setup up the soil model.""" @@ -202,6 +213,9 @@ def _setup( # both the soil and abiotic models get more complex this might well change. self.model_constants = model_constants + # Store set of microbial functional groups needed by the model + self.microbial_groups = microbial_groups + # Calculate dissolved amounts of each inorganic nutrient dissolved_nutrient_pools = self.calculate_dissolved_nutrient_concentrations() # Update the data object with these pools @@ -309,6 +323,7 @@ def integrate(self) -> dict[str, DataArray]: self.layer_structure.index_topsoil_scalar, delta_pools_ordered, self.model_constants, + self.microbial_groups, self.core_constants.max_depth_of_microbial_activity, self.core_constants.soil_moisture_capacity, self.layer_structure.soil_layer_thickness[0], @@ -379,6 +394,7 @@ def construct_full_soil_model( top_soil_layer_index: int, delta_pools_ordered: dict[str, NDArray[np.float32]], model_constants: SoilConsts, + functional_groups: dict[str, MicrobialGroupConstants], max_depth_of_microbial_activity: float, soil_moisture_capacity: float, top_soil_layer_thickness: float, @@ -396,6 +412,7 @@ def construct_full_soil_model( delta_pools_ordered: Dictionary to store pool changes in the order that pools are stored in the initial condition vector. model_constants: Set of constants for the soil model. + functional_groups: Set of microbial functional groups used by the soil model. max_depth_of_microbial_activity: Maximum depth of the soil profile where microbial activity occurs [m]. soil_moisture_capacity: Soil moisture capacity, i.e. the maximum @@ -418,6 +435,7 @@ def construct_full_soil_model( data, pools=all_pools, constants=model_constants, + functional_groups=functional_groups, max_depth_of_microbial_activity=max_depth_of_microbial_activity, )