From ba681858e093cc6aa37b497a4fc0bbf3807c757e Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 4 Dec 2024 09:27:46 +0000 Subject: [PATCH 1/8] Added organic P pools into the soil model --- tests/conftest.py | 4 +++ tests/core/test_data.py | 4 +++ tests/models/soil/test_soil_model.py | 33 ++++++++++++++++++ tests/test_main.py | 4 +++ virtual_ecosystem/data_variables.toml | 28 +++++++++++++++ .../example_data/config/data_config.toml | 12 +++++++ .../example_data/data/example_soil_data.nc | Bin 26097 -> 32050 bytes .../generation_scripts/soil_example_data.py | 19 ++++++++++ virtual_ecosystem/models/soil/pools.py | 13 +++++++ virtual_ecosystem/models/soil/soil_model.py | 12 +++++++ 10 files changed, 129 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index c3617d076..7ba6ae566 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -264,6 +264,10 @@ def dummy_carbon_data(fixture_core_components): "soil_n_pool_particulate": [0.00714285, 0.00071425, 0.00285714, 0.01428571], "soil_n_pool_necromass": [0.00288462, 0.01788462, 0.02019231, 0.01115385], "soil_n_pool_maom": [0.86538462, 0.48076923, 0.32692308, 0.09615385], + "soil_p_pool_dop": [5.714e-6, 2.2857120e-5, 5.7142800e-5, 1.1428568e-4], + "soil_p_pool_particulate": [2.857e-5, 2.85714e-4, 1.142856e-4, 5.714284e-4], + "soil_p_pool_necromass": [0.00080769, 0.00011538, 0.00071538, 0.00044615], + "soil_p_pool_maom": [0.01307692, 0.03461538, 0.01923077, 0.00384615], "pH": [3.0, 7.5, 9.0, 5.7], "bulk_density": [1350.0, 1800.0, 1000.0, 1500.0], "clay_fraction": [0.8, 0.3, 0.1, 0.9], diff --git a/tests/core/test_data.py b/tests/core/test_data.py index 05a983ae4..cb7cbdfbd 100644 --- a/tests/core/test_data.py +++ b/tests/core/test_data.py @@ -978,6 +978,10 @@ def test_output_current_state(mocker, dummy_carbon_data, time_index): "soil_n_pool_particulate", "soil_n_pool_necromass", "soil_n_pool_maom", + "soil_p_pool_dop", + "soil_p_pool_particulate", + "soil_p_pool_necromass", + "soil_p_pool_maom", ], time_index, ) diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index ca07c812a..890027cdd 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -25,6 +25,10 @@ (DEBUG, "soil model: required var 'soil_n_pool_particulate' checked"), (DEBUG, "soil model: required var 'soil_n_pool_necromass' checked"), (DEBUG, "soil model: required var 'soil_n_pool_maom' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_dop' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_particulate' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_necromass' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_maom' checked"), (DEBUG, "soil model: required var 'pH' checked"), (DEBUG, "soil model: required var 'bulk_density' checked"), (DEBUG, "soil model: required var 'clay_fraction' checked"), @@ -318,6 +322,19 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): soil_n_pool_maom=DataArray( [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), + soil_p_pool_dop=DataArray( + [5.714e-6, 2.2857120e-5, 5.7142800e-5, 1.1428568e-4], + dims="cell_id", + ), + soil_p_pool_particulate=DataArray( + [2.857e-5, 2.85714e-4, 1.142856e-4, 5.714284e-4], dims="cell_id" + ), + soil_p_pool_necromass=DataArray( + [0.00080769, 0.00011538, 0.00071538, 0.00044615], dims="cell_id" + ), + soil_p_pool_maom=DataArray( + [0.01307692, 0.03461538, 0.01923077, 0.00384615], dims="cell_id" + ), ) ), (), @@ -480,6 +497,22 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, ] # make pools diff --git a/tests/test_main.py b/tests/test_main.py index a388bbe0e..ed82ae103 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -32,6 +32,10 @@ (DEBUG, "soil model: required var 'soil_n_pool_particulate' checked"), (DEBUG, "soil model: required var 'soil_n_pool_necromass' checked"), (DEBUG, "soil model: required var 'soil_n_pool_maom' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_dop' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_particulate' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_necromass' checked"), + (DEBUG, "soil model: required var 'soil_p_pool_maom' checked"), (DEBUG, "soil model: required var 'pH' checked"), (DEBUG, "soil model: required var 'bulk_density' checked"), (DEBUG, "soil model: required var 'clay_fraction' checked"), diff --git a/virtual_ecosystem/data_variables.toml b/virtual_ecosystem/data_variables.toml index 8bdafdb69..a5e4d7968 100644 --- a/virtual_ecosystem/data_variables.toml +++ b/virtual_ecosystem/data_variables.toml @@ -600,6 +600,34 @@ name = "soil_n_pool_maom" unit = "kg N m^-3" variable_type = "float" +[[variable]] +axis = ["spatial"] +description = "Dissolved organic phosphorus pool" +name = "soil_p_pool_dop" +unit = "kg P m^-3" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Particulate organic phosphorus pool" +name = "soil_p_pool_particulate" +unit = "kg P m^-3" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Amount of phosphorus contained in the necromass pool" +name = "soil_p_pool_necromass" +unit = "kg P m^-3" +variable_type = "float" + +[[variable]] +axis = ["spatial"] +description = "Amount of phosphorus contained in the mineral associated organic matter pool" +name = "soil_p_pool_maom" +unit = "kg P m^-3" +variable_type = "float" + [[variable]] axis = ["spatial"] description = "Bulk density of soil" diff --git a/virtual_ecosystem/example_data/config/data_config.toml b/virtual_ecosystem/example_data/config/data_config.toml index 6bc9c5cb1..f6dc4a2ba 100644 --- a/virtual_ecosystem/example_data/config/data_config.toml +++ b/virtual_ecosystem/example_data/config/data_config.toml @@ -77,6 +77,18 @@ var_name = "soil_n_pool_necromass" [[core.data.variable]] file = "../data/example_soil_data.nc" var_name = "soil_n_pool_maom" +[[core.data.variable]] +file = "../data/example_soil_data.nc" +var_name = "soil_p_pool_dop" +[[core.data.variable]] +file = "../data/example_soil_data.nc" +var_name = "soil_p_pool_particulate" +[[core.data.variable]] +file = "../data/example_soil_data.nc" +var_name = "soil_p_pool_necromass" +[[core.data.variable]] +file = "../data/example_soil_data.nc" +var_name = "soil_p_pool_maom" # Litter [[core.data.variable]] diff --git a/virtual_ecosystem/example_data/data/example_soil_data.nc b/virtual_ecosystem/example_data/data/example_soil_data.nc index cb06a00f0979084e5e67c2d3247a347e6e2bfdc4..581bf0df22b222f4f4be3af565162e638f1eb8a1 100644 GIT binary patch delta 5629 zcmb_gYgiRk7v3`*;CKYN2udW1fM6uS{qUTbz2;r-de@r0_SuK_ zv&^1nOi>RHzf2?!U+IDh54Dzt=|je|R(n}D(Pq}IIB2KljUEMeNJYq2oz|XO%@|KT zsR`m-P7hA(f{^g19zK@78I%v@9R0&wX$PQ7&&T_IgGWzw}xEPb+?+xpk0)E+3-V)3>~^BjMEZfW=g(PnA^wh4<_dF!hGmq^CjK@_c579S*O4&u*EM&|{LM+TjVDjb zNrzUtpqPcy4J|7jU@xM2g^?wG8ts6m)u2O-r-{)4cEHmFg~2NiAOKB=4z+`nmeCIQ zWfJ@^_~j});PYOv3u%I){1+Y|1P%o%JK$HS?0{dXvID+QWf$?Z%&Sxnz+tt@4grc( zcEA^_?0_#(*#S=kyZD-+)ZkG1e;n4R?0~0Bj1J&|0P7^PVh8+sqg`5N;IP5?K)eHo zjVe3fKT_ENU#7AH{$rKBh?U}_O{xdruvuk?0Ocw>;J2vkfS1jv$O`V{tKouurj^0M z)z(@&Q0#w%$gn5};^#d;Q@o<^sl08ZvX!BRIlf&PPrBSqHwtN)w?1x~Liaqnw;|Mu z$P}Nc&UE3LLZt;eB-ta%@STr=ai*PM`dn%Nzql#Huxa2$ov!Yz-v~5R=zBMk64S}4&tZy^c8V>{};n= z#f`fFdXPlTiU16rFNeioIP1G27N#81F7dv9Vwf>pnG}w;fhkm4{yUF{MiGvUAmQ=u1~dVKt2w2}USNbgCT#uO?wOXtA7 zX-%rfC$l+HaA&$xnKIHHN?6WG2@lAof$x%|6RXHk?^yP-45#~)DO}p0#?{}LOyO)r z2-)~%HhVOPxCFXrE@bk(432NRX_X^RUBf_F`yz|qmg5_9|+Zs{NfKXA7 z&j_l=OS38_#mM(v@Vp5xu0~H!l&?Ga6_%TT|q$xRhBR$4P=14K$#m2(g#r%}z{6OPLin zcW!&4(9eF1eD0I7A39!g7PA+&RrC0*+ec2AFap_n%dF-Ib#XJoLjWDea4-< zeZStt|JHTQFT6#}`myzRa8AELAC}kSI=ia>wfCt>9_K^+6+&#R>YE{Xk#(TKI>oRs(-GsgQ z;2!sXdZSq(0s%l^NN~6P;(nvzSg`L`2XvFfZhGgDLIke>N(dEz1V8}ze}4Sh@S>%UaJ*lnZ@=w7D?CzV1~+0T2~+|q02zRQ z7YQDd(bGopGsS8>$G7Gbth)Q?xtu-X5(^jRjr;&{<^GP_efJ~o#_caoJ3m33u2_{XYx`Jd${$AK(GU4BD4m;lpwZ@|!END_EXux!J2VvcDN8 zi8E7(KmZV&DsUeART~42oL63zO;C~)PgV8#mCA&lL#r zFSxDXGMYRGYIm(vct8jU5aaSfU7sMY%Wk7Y2FbBSIGoL3K3L5${?2m0pNdH zU~%>-$_9Ve82twIE)fr@|-X^|Bv8Av#hK=haJ>p9_|JEIFdUcrAi#=J)l zE%rCKH6Ets+L@yH>b>iArsgThqIu8C54M?_W74SEInQH>aBlC8lgBXLd9L)zt>X$2 z2mt{Q7y@p8Co0@>hSBfR@58TuKIsHbdh3wO{PQOjo>^i1pkA~BkO2q)g3q|-#4|&` z!sUGye^}V{B>t$geQ|ixDZIA*^Q~R0zfp)FBM<@!fN=0f1pqpiAGWbwN;EHYORSx>yJhwNkb$g^-oESGk5MRmm8U6_uTuP?>y$r znVI|PZL+pVPDaPAHGw)(xqzve($f{5qH4)DBW+hPQ~0U84};4d9R5nh?SQXI`7xoP zOj%LP%?h{xS2W1<(G-qj!BOZi6=2-p{Z6oIYE9U0UdzIDQi-#r#XG&nTfGhKO}pJ< z?1lTTpDWU05#nS<5+MZV(;~8BW7GfuZiuiz#0b@yB_K4Um1p!<01qmPCTEuTu@Qyd6?TCHgi>u#t2XoB{I!L9c17= zNqeNC-~sz8v$Z_|G+Psx!NMj|p1;2s98{l2e~ez+(%S8A?apy~-M*%t!lDv)V{1o~ zue-I==Wgijar-)ZHdK9E)NV4kMVXU_Nn``{sOFLV;S>QA z`7Vn|01u$IGd-HwuvwZRy`I4UWnx;*IOb@R7pDhJN`K%Cv0WyR!xFzW;Ty%b%x^?C z(ieS9)?bTfvxadcs5vAQv`pZ~xrsWgw?^t9&z1F~IGd2##jU~swT<`s858~t$8BhBitud zFodcGa2s_xtd83X#P*MfyeAi7bMWfrSR8B0oiv{w?CC3+rZmDBi_a zzW$@_)att~eqpRs@fg>*z?V$1F4jZW(`!xx{Mqrj!tcCUyp;Sn)(2apcn@BZ2XVBa z41WkI4`Vw(AEaA7qiWjcR`9v{I^5$eRfuPog={#uP`1ed+Y)*Qz05(>%E7ny>hFU0 zlX!yaU62{c?}1@22MzbYi_iYHh;!BLq)v(5?Iclb6X_$c%|p+KBjzcbN^8dX+F4z8 z=?6At$7-Rs9Z;t?ztS9s^IbcFY^o~&8wL(7C@BXl7PBpzKp0LB?hg{TbqN05wQS#a zU@}v^Z2w0eIJ6vG`OuP&*V?K3mU&0}a?sYt|K%XQWzOyZxbsc=Q#_BWg*`Q!TZi`U Rqmj|07|@ugyx_P@{sTEhe+2*l 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 d3d81fcca..e4b55268e 100644 --- a/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py +++ b/virtual_ecosystem/example_data/generation_scripts/soil_example_data.py @@ -59,6 +59,21 @@ # nitrogen pool [kg N m^-3]. necromass_n_values = 3e-5 + 7e-5 * gradient / 64.0 +# Generate a range of plausible values (1e-5 - 2e-5) for the DOP pool [kg P m^-3] +dop_values = 1e-5 + 1e-5 * gradient / 64.0 + +# Generate a range of plausible values (3e-5 - 6e-5) for the particulate P pool [kg P +# m^-3] +particulate_p_values = 3e-5 + 3e-5 * gradient / 64.0 + +# Generate a range of plausible values (0.008-0.024) for the maom phosphorus pool [kg P +# m^-3]. +maom_p_values = 0.008 + 0.016 * gradient / 64.0 + +# Generate a range of plausible values (1.2e-6 - 4e-6) for the microbial necromass +# phosphorus pool [kg P m^-3]. +necromass_p_values = 1.2e-6 + 2.8e-6 * gradient / 64.0 + # Make example soil dataset example_soil_data = Dataset( data_vars=dict( @@ -76,6 +91,10 @@ soil_n_pool_particulate=(["x", "y"], particulate_n_values), soil_n_pool_maom=(["x", "y"], maom_n_values), soil_n_pool_necromass=(["x", "y"], necromass_n_values), + soil_p_pool_dop=(["x", "y"], dop_values), + soil_p_pool_particulate=(["x", "y"], particulate_p_values), + soil_p_pool_maom=(["x", "y"], maom_p_values), + soil_p_pool_necromass=(["x", "y"], necromass_p_values), ), coords=dict( x=(["x"], cell_displacements), diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index baa3def1f..77b035391 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -286,6 +286,19 @@ def calculate_all_pool_updates( delta_pools_ordered["soil_n_pool_maom"] = ( necromass_n_sorption - nitrogen_transfer_maom_to_don ) + # TODO - Fill the below in with real values + delta_pools_ordered["soil_p_pool_dop"] = np.zeros_like( + delta_pools_ordered["soil_n_pool_maom"] + ) + delta_pools_ordered["soil_p_pool_particulate"] = np.zeros_like( + delta_pools_ordered["soil_n_pool_maom"] + ) + delta_pools_ordered["soil_p_pool_necromass"] = np.zeros_like( + delta_pools_ordered["soil_n_pool_maom"] + ) + delta_pools_ordered["soil_p_pool_maom"] = np.zeros_like( + delta_pools_ordered["soil_n_pool_maom"] + ) delta_pools_ordered["soil_enzyme_pom"] = microbial_changes.pom_enzyme_change delta_pools_ordered["soil_enzyme_maom"] = microbial_changes.maom_enzyme_change diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index 6ad581e90..131c4481b 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -56,6 +56,10 @@ class SoilModel( "soil_n_pool_particulate", "soil_n_pool_necromass", "soil_n_pool_maom", + "soil_p_pool_dop", + "soil_p_pool_particulate", + "soil_p_pool_necromass", + "soil_p_pool_maom", "pH", "bulk_density", "clay_fraction", @@ -73,6 +77,10 @@ class SoilModel( "soil_n_pool_particulate", "soil_n_pool_necromass", "soil_n_pool_maom", + "soil_p_pool_dop", + "soil_p_pool_particulate", + "soil_p_pool_necromass", + "soil_p_pool_maom", "matric_potential", "vertical_flow", "soil_temperature", @@ -92,6 +100,10 @@ class SoilModel( "soil_n_pool_particulate", "soil_n_pool_necromass", "soil_n_pool_maom", + "soil_p_pool_dop", + "soil_p_pool_particulate", + "soil_p_pool_necromass", + "soil_p_pool_maom", ), vars_populated_by_first_update=(), ): From d5eb80a44b4cfc5ad7771a48934a7f5176d8255a Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 4 Dec 2024 10:01:23 +0000 Subject: [PATCH 2/8] Added phosphorus decay from litter into the soil model --- tests/conftest.py | 1 + tests/models/soil/test_pools.py | 4 ++++ tests/models/soil/test_soil_model.py | 20 +++++++++++--------- virtual_ecosystem/models/soil/constants.py | 7 +++++++ virtual_ecosystem/models/soil/pools.py | 16 ++++++++++------ virtual_ecosystem/models/soil/soil_model.py | 1 + 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7ba6ae566..1f819ae5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -273,6 +273,7 @@ def dummy_carbon_data(fixture_core_components): "clay_fraction": [0.8, 0.3, 0.1, 0.9], "litter_C_mineralisation_rate": [0.00212106, 0.00106053, 0.00049000, 0.0055], "litter_N_mineralisation_rate": [3.5351e-5, 7.0702e-5, 0.000183, 1.63333e-5], + "litter_P_mineralisation_rate": [7.32e-6, 1.41404e-6, 2.82808e-6, 6.53332e-7], "vertical_flow": [0.1, 0.5, 2.5, 1.59], } diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index ca23af31a..f85db54f5 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -47,6 +47,10 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], + "soil_p_pool_dop": [7.32e-10, 1.41404e-10, 2.82808e-10, 6.53332e-11], + "soil_p_pool_particulate": [7.31927e-6, 1.4139e-6, 2.8277972e-6, 6.5326667e-7], + "soil_p_pool_necromass": [0.0, 0.0, 0.0, 0.0], + "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], } # Make order of pools object diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 890027cdd..8c80a7372 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -327,7 +327,8 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): dims="cell_id", ), soil_p_pool_particulate=DataArray( - [2.857e-5, 2.85714e-4, 1.142856e-4, 5.714284e-4], dims="cell_id" + [3.2229634e-5, 2.86420949e-4, 1.156995e-4, 5.71755033e-4], + dims="cell_id", ), soil_p_pool_necromass=DataArray( [0.00080769, 0.00011538, 0.00071538, 0.00044615], dims="cell_id" @@ -415,6 +416,7 @@ def test_order_independance( "clay_fraction", "litter_C_mineralisation_rate", "litter_N_mineralisation_rate", + "litter_P_mineralisation_rate", ] for not_pool in not_pools: new_data[not_pool] = dummy_carbon_data[not_pool] @@ -497,14 +499,14 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 7.32e-10, + 1.41404e-10, + 2.82808e-10, + 6.53332e-11, + 7.31927e-6, + 1.4139e-6, + 2.8277972e-6, + 6.5326667e-7, 0.0, 0.0, 0.0, diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 92178c8b4..5aeefc06e 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -310,6 +310,13 @@ class SoilConsts(ConstantsDataclass): an order of magnitude estimate taken from :cite:t:`fatichi_mechanistic_2019`. """ + litter_leaching_fraction_phosphorus = 0.0001 + """Fraction of phosphorus mineralisation from litter that occurs by leaching. + + [unitless]. The remainder of the mineralisation consists of particulates. Value is + an order of magnitude estimate taken from :cite:t:`fatichi_mechanistic_2019`. + """ + microbial_c_n_ratio = 5.2 """Ratio of carbon to nitrogen in microbial biomass [unitless]. diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 77b035391..b1e856db3 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -208,6 +208,10 @@ def calculate_all_pool_updates( mineralisation_rate=self.data["litter_N_mineralisation_rate"].to_numpy(), litter_leaching_coefficient=self.constants.litter_leaching_fraction_nitrogen, ) + litter_mineralisation_fluxes_P = calculate_litter_mineralisation_split( + mineralisation_rate=self.data["litter_P_mineralisation_rate"].to_numpy(), + litter_leaching_coefficient=self.constants.litter_leaching_fraction_phosphorus, + ) # Find mineralisation rate from POM pom_n_mineralisation = calculate_soil_nutrient_mineralisation( @@ -287,12 +291,12 @@ def calculate_all_pool_updates( necromass_n_sorption - nitrogen_transfer_maom_to_don ) # TODO - Fill the below in with real values - delta_pools_ordered["soil_p_pool_dop"] = np.zeros_like( - delta_pools_ordered["soil_n_pool_maom"] - ) - delta_pools_ordered["soil_p_pool_particulate"] = np.zeros_like( - delta_pools_ordered["soil_n_pool_maom"] - ) + delta_pools_ordered["soil_p_pool_dop"] = litter_mineralisation_fluxes_P[ + "dissolved" + ] + delta_pools_ordered["soil_p_pool_particulate"] = litter_mineralisation_fluxes_P[ + "particulate" + ] delta_pools_ordered["soil_p_pool_necromass"] = np.zeros_like( delta_pools_ordered["soil_n_pool_maom"] ) diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index 131c4481b..19919f81d 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -87,6 +87,7 @@ class SoilModel( "soil_moisture", "litter_C_mineralisation_rate", "litter_N_mineralisation_rate", + "litter_P_mineralisation_rate", ), vars_updated=( "soil_c_pool_maom", From ae04bfce585aa934a5ecd28ed2122cf279e30c71 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 4 Dec 2024 15:24:21 +0000 Subject: [PATCH 3/8] Added leaching of dissolved organic phosphorus to the soil model --- tests/models/soil/test_pools.py | 2 +- tests/models/soil/test_soil_model.py | 10 +++++----- virtual_ecosystem/models/soil/constants.py | 7 +++++++ virtual_ecosystem/models/soil/pools.py | 12 +++++++++--- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index f85db54f5..68906d6e3 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -47,7 +47,7 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_p_pool_dop": [7.32e-10, 1.41404e-10, 2.82808e-10, 6.53332e-11], + "soil_p_pool_dop": [-1.7244141e-9, -5.7904789e-8, -1.1329112e-6, -2.4025466e-6], "soil_p_pool_particulate": [7.31927e-6, 1.4139e-6, 2.8277972e-6, 6.5326667e-7], "soil_p_pool_necromass": [0.0, 0.0, 0.0, 0.0], "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 8c80a7372..822f0f866 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -323,7 +323,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), soil_p_pool_dop=DataArray( - [5.714e-6, 2.2857120e-5, 5.7142800e-5, 1.1428568e-4], + [5.71313789e-6, 2.28281860e-5, 5.65791435e-5, 1.13090698e-4], dims="cell_id", ), soil_p_pool_particulate=DataArray( @@ -499,10 +499,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - 7.32e-10, - 1.41404e-10, - 2.82808e-10, - 6.53332e-11, + -1.7244141e-9, + -5.7904789e-8, + -1.1329112e-6, + -2.4025466e-6, 7.31927e-6, 1.4139e-6, 2.8277972e-6, diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 5aeefc06e..d6d43e20c 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -263,6 +263,13 @@ class SoilConsts(ConstantsDataclass): a loose manner. """ + solubility_coefficient_dop: float = 1.0 + """Solubility coefficient for dissolved organic phosphorus [unitless]. + + Value taken from :cite:t:`fatichi_mechanistic_2019`, where it is estimated in quite + a loose manner. + """ + necromass_decay_rate: float = (1 / 3) * np.log(2) """Rate at which microbial necromass decays to low molecular weight carbon [day^-1] diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index b1e856db3..d0774a327 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -177,6 +177,12 @@ def calculate_all_pool_updates( soil_moisture=soil_moisture, solubility_coefficient=self.constants.solubility_coefficient_don, ) + dop_leaching = calculate_leaching_rate( + solute_density=self.pools["soil_p_pool_dop"], + vertical_flow_rate=self.data["vertical_flow"].to_numpy(), + soil_moisture=soil_moisture, + solubility_coefficient=self.constants.solubility_coefficient_dop, + ) # Calculate transfers between the lmwc, necromass and maom pools maom_desorption_to_lmwc = calculate_maom_desorption( @@ -291,9 +297,9 @@ def calculate_all_pool_updates( necromass_n_sorption - nitrogen_transfer_maom_to_don ) # TODO - Fill the below in with real values - delta_pools_ordered["soil_p_pool_dop"] = litter_mineralisation_fluxes_P[ - "dissolved" - ] + delta_pools_ordered["soil_p_pool_dop"] = ( + litter_mineralisation_fluxes_P["dissolved"] - dop_leaching + ) delta_pools_ordered["soil_p_pool_particulate"] = litter_mineralisation_fluxes_P[ "particulate" ] From a339c0cdcc065f0fe9f75626091f662d2c58f495 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 4 Dec 2024 15:41:23 +0000 Subject: [PATCH 4/8] Added phosphorus flow due to soil organic matter mineralisation --- tests/models/soil/test_pools.py | 4 ++-- tests/models/soil/test_soil_model.py | 20 ++++++++++---------- virtual_ecosystem/models/soil/pools.py | 17 ++++++++++++----- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 68906d6e3..84a336450 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -47,8 +47,8 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_p_pool_dop": [-1.7244141e-9, -5.7904789e-8, -1.1329112e-6, -2.4025466e-6], - "soil_p_pool_particulate": [7.31927e-6, 1.4139e-6, 2.8277972e-6, 6.5326667e-7], + "soil_p_pool_dop": [9.53691781e-8, 2.49063642e-6, 9.08802987e-7, -2.3349143e-6], + "soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7], "soil_p_pool_necromass": [0.0, 0.0, 0.0, 0.0], "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], } diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 822f0f866..99b7df00b 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -323,11 +323,11 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), soil_p_pool_dop=DataArray( - [5.71313789e-6, 2.28281860e-5, 5.65791435e-5, 1.13090698e-4], + [5.76474498e-6, 2.41003861e-5, 5.75967069e-5, 1.13124345e-4], dims="cell_id", ), soil_p_pool_particulate=DataArray( - [3.2229634e-5, 2.86420949e-4, 1.156995e-4, 5.71755033e-4], + [3.21780215e-5, 2.85147941e-4, 1.14676885e-4, 5.71721209e-4], dims="cell_id", ), soil_p_pool_necromass=DataArray( @@ -499,14 +499,14 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - -1.7244141e-9, - -5.7904789e-8, - -1.1329112e-6, - -2.4025466e-6, - 7.31927e-6, - 1.4139e-6, - 2.8277972e-6, - 6.5326667e-7, + 9.53691781e-8, + 2.49063642e-6, + 9.08802987e-7, + -2.3349143e-6, + 7.22218e-6, + -1.13464e-6, + 7.86083e-7, + 5.85634364e-7, 0.0, 0.0, 0.0, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index d0774a327..4b162782e 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -219,12 +219,17 @@ def calculate_all_pool_updates( litter_leaching_coefficient=self.constants.litter_leaching_fraction_phosphorus, ) - # Find mineralisation rate from POM + # Find mineralisation rates from POM pom_n_mineralisation = calculate_soil_nutrient_mineralisation( pool_carbon=self.pools["soil_c_pool_pom"], pool_nutrient=self.pools["soil_n_pool_particulate"], breakdown_rate=enzyme_mediated.pom_to_lmwc, ) + pom_p_mineralisation = calculate_soil_nutrient_mineralisation( + pool_carbon=self.pools["soil_c_pool_pom"], + pool_nutrient=self.pools["soil_p_pool_particulate"], + breakdown_rate=enzyme_mediated.pom_to_lmwc, + ) # Find flow of nitrogen to necromass pool necromass_n_flow = calculate_nutrient_flows_to_necromass( @@ -298,11 +303,13 @@ def calculate_all_pool_updates( ) # TODO - Fill the below in with real values delta_pools_ordered["soil_p_pool_dop"] = ( - litter_mineralisation_fluxes_P["dissolved"] - dop_leaching + litter_mineralisation_fluxes_P["dissolved"] + + pom_p_mineralisation + - dop_leaching + ) + delta_pools_ordered["soil_p_pool_particulate"] = ( + litter_mineralisation_fluxes_P["particulate"] - pom_p_mineralisation ) - delta_pools_ordered["soil_p_pool_particulate"] = litter_mineralisation_fluxes_P[ - "particulate" - ] delta_pools_ordered["soil_p_pool_necromass"] = np.zeros_like( delta_pools_ordered["soil_n_pool_maom"] ) From 504cf6f8cc8b14588d2acd6307c86f188dbee439 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Thu, 5 Dec 2024 16:04:24 +0000 Subject: [PATCH 5/8] Added phosphorus flow associated with microbial death --- tests/models/soil/test_pools.py | 10 ++++++--- tests/models/soil/test_soil_model.py | 10 ++++----- virtual_ecosystem/models/soil/constants.py | 8 +++++++ virtual_ecosystem/models/soil/pools.py | 26 ++++++++++++---------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 84a336450..569f402a5 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -49,7 +49,7 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "soil_n_pool_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], "soil_p_pool_dop": [9.53691781e-8, 2.49063642e-6, 9.08802987e-7, -2.3349143e-6], "soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7], - "soil_p_pool_necromass": [0.0, 0.0, 0.0, 0.0], + "soil_p_pool_necromass": [0.0034213, 0.00143969, 0.00747022, 0.00045376], "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], } @@ -413,12 +413,16 @@ def test_calculate_nutrient_flows_to_necromass(microbial_changes): ) 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] - actual_n_flow_to_necromass = calculate_nutrient_flows_to_necromass( - microbial_changes=microbial_changes, constants=SoilConsts + actual_n_flow_to_necromass, actual_p_flow_to_necromass = ( + calculate_nutrient_flows_to_necromass( + microbial_changes=microbial_changes, constants=SoilConsts + ) ) assert np.allclose(actual_n_flow_to_necromass, expected_n_flow_to_necromass) + assert np.allclose(actual_p_flow_to_necromass, expected_p_flow_to_necromass) def test_find_necromass_nitrogen_outflows( diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 99b7df00b..fea21f93e 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -331,7 +331,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): dims="cell_id", ), soil_p_pool_necromass=DataArray( - [0.00080769, 0.00011538, 0.00071538, 0.00044615], dims="cell_id" + [0.00251446, 0.00083368, 0.00444109, 0.00067262], dims="cell_id" ), soil_p_pool_maom=DataArray( [0.01307692, 0.03461538, 0.01923077, 0.00384615], dims="cell_id" @@ -507,10 +507,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -1.13464e-6, 7.86083e-7, 5.85634364e-7, - 0.0, - 0.0, - 0.0, - 0.0, + 0.0034213, + 0.00143969, + 0.00747022, + 0.00045376, 0.0, 0.0, 0.0, diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index d6d43e20c..b4a99c687 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -332,6 +332,14 @@ class SoilConsts(ConstantsDataclass): added this constant needs to be split. """ + microbial_c_p_ratio = 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. + """ + max_uptake_rate_don = 0.0077 """Maximum rate at for dissolved organic nitrogen uptake [day^-1]. diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 4b162782e..83a4a2fa9 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -1,9 +1,9 @@ """The ``models.soil.pools`` module simulates all soil pools for the Virtual -Ecosystem. At the moment four carbon pools are modelled (low molecular weight carbon +Ecosystem. At the moment five carbon pools are modelled (low molecular weight carbon (LMWC), mineral associated organic matter (MAOM), microbial biomass, particulate organic -matter (POM)), as well as two enzyme pools (POM and MAOM) degrading enzymes, and two -nitrogen pools (dissolved organic nitrogen (DON) and particulate organic nitrogen -(PON)). +matter (POM), microbial necromass), as well as two enzyme pools (POM and MAOM) degrading +enzymes. Pools that track the nitrogen and phosphorus pools associated with each of the +carbon pools are also included. """ # noqa: D205 from dataclasses import dataclass @@ -144,6 +144,7 @@ def calculate_all_pool_updates( constants=self.constants, ) # find changes related to microbial uptake, growth and decay + # TODO - Add phosphorus changes in here microbial_changes = calculate_microbial_changes( soil_c_pool_lmwc=self.pools["soil_c_pool_lmwc"], soil_n_pool_don=self.pools["soil_n_pool_don"], @@ -232,7 +233,7 @@ def calculate_all_pool_updates( ) # Find flow of nitrogen to necromass pool - necromass_n_flow = calculate_nutrient_flows_to_necromass( + 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 @@ -301,7 +302,6 @@ def calculate_all_pool_updates( delta_pools_ordered["soil_n_pool_maom"] = ( necromass_n_sorption - nitrogen_transfer_maom_to_don ) - # TODO - Fill the below in with real values delta_pools_ordered["soil_p_pool_dop"] = ( litter_mineralisation_fluxes_P["dissolved"] + pom_p_mineralisation @@ -310,9 +310,7 @@ def calculate_all_pool_updates( delta_pools_ordered["soil_p_pool_particulate"] = ( litter_mineralisation_fluxes_P["particulate"] - pom_p_mineralisation ) - delta_pools_ordered["soil_p_pool_necromass"] = np.zeros_like( - delta_pools_ordered["soil_n_pool_maom"] - ) + delta_pools_ordered["soil_p_pool_necromass"] = necromass_p_flow delta_pools_ordered["soil_p_pool_maom"] = np.zeros_like( delta_pools_ordered["soil_n_pool_maom"] ) @@ -910,7 +908,7 @@ def calculate_soil_nutrient_mineralisation( def calculate_nutrient_flows_to_necromass( microbial_changes: MicrobialChanges, constants: SoilConsts -) -> NDArray[np.float32]: +) -> 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 @@ -927,10 +925,14 @@ def calculate_nutrient_flows_to_necromass( constants: Set of constants for the soil model. Returns: - The rate at which nitrogen is added to the necromass pool [kg N m^-3 day^-1] + 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 """ - return microbial_changes.necromass_generation / constants.microbial_c_n_ratio + return ( + microbial_changes.necromass_generation / constants.microbial_c_n_ratio, + microbial_changes.necromass_generation / constants.microbial_c_p_ratio, + ) def find_necromass_nitrogen_outflows( From e126743b3b9bc31f2337de544e81c4f8a57d89ce Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Mon, 9 Dec 2024 11:59:40 +0000 Subject: [PATCH 6/8] Added microbial uptake of phosphorus to set of nutrients determining growth rate --- tests/models/soil/conftest.py | 1 + tests/models/soil/test_pools.py | 62 +++++++++++-------- tests/models/soil/test_soil_model.py | 40 ++++++------ virtual_ecosystem/models/soil/constants.py | 24 +++++++- virtual_ecosystem/models/soil/pools.py | 72 +++++++++++++++------- 5 files changed, 131 insertions(+), 68 deletions(-) diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index 0d30eb678..9690e25aa 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -109,6 +109,7 @@ def microbial_changes( return calculate_microbial_changes( soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"], soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"], + soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"], soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"], diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 569f402a5..036980673 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -36,18 +36,18 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): soil_pools = SoilPools(data=dummy_carbon_data, pools=pools, constants=SoilConsts) change_in_pools = { - "soil_c_pool_lmwc": [0.0129365, 0.0060499, 0.03697928, 0.02425546], + "soil_c_pool_lmwc": [0.01510858, 0.01400719, 0.03697928, 0.02426899], "soil_c_pool_maom": [0.038767651, 0.00829848, 0.05982197, 0.07277182], - "soil_c_pool_microbe": [-0.05362396, -0.02020101, -0.11965575, -0.00719517], + "soil_c_pool_microbe": [-0.0544059, -0.02282691, -0.11965575, -0.00720166], "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.00104884, 0.00419763, 0.00496839, 0.00251317], + "soil_n_pool_don": [0.00119921, 0.00470261, 0.00496839, 0.00251442], "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_p_pool_dop": [9.53691781e-8, 2.49063642e-6, 9.08802987e-7, -2.3349143e-6], + "soil_p_pool_dop": [-1.4593572e-6, -7.3316032e-6, -2.8267967e-5, -3.6612053e-6], "soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7], "soil_p_pool_necromass": [0.0034213, 0.00143969, 0.00747022, 0.00045376], "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], @@ -76,9 +76,10 @@ def test_calculate_microbial_changes( from virtual_ecosystem.models.soil.pools import calculate_microbial_changes - expected_lmwc_uptake = [2.241176e-3, 8.433524e-3, 1.556094e-3, 5.7736357e-5] - expected_don_uptake = [1.5515837e-4, 5.3520443e-4, 8.97746776e-5, 5.3295099e-6] - expected_microbe = [-0.05362396, -0.02020101, -0.11965575, -0.00719517] + expected_lmwc_uptake = [6.90989514e-5, 4.76229800e-4, 1.55609440e-3, 4.42097002e-5] + expected_don_uptake = [4.78377356e-6, 3.02222758e-5, 8.97746767e-5, 4.08089540e-6] + expected_dop_uptake = [1.55472641e-6, 9.82223962e-6, 2.91767699e-5, 1.32629101e-6] + expected_microbe = [-0.0544059, -0.02282691, -0.11965575, -0.00720166] expected_pom_enzyme = [1.17571917e-8, 1.67442231e-8, 1.83311362e-9, -1.11675865e-8] expected_maom_enzyme = [-3.1009224e-4, -5.0959256e-5, 5.9906583e-4, -3.7211168e-5] expected_necromass = [0.05474086, 0.02303502, 0.11952352, 0.00726011] @@ -86,6 +87,7 @@ def test_calculate_microbial_changes( mic_changes = calculate_microbial_changes( soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"], soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"], + soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"], soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"], @@ -99,6 +101,7 @@ def test_calculate_microbial_changes( # Check that each rate matches expectation assert np.allclose(mic_changes.lmwc_uptake, expected_lmwc_uptake) assert np.allclose(mic_changes.don_uptake, expected_don_uptake) + assert np.allclose(mic_changes.dop_uptake, expected_dop_uptake) assert np.allclose(mic_changes.microbe_change, expected_microbe) assert np.allclose(mic_changes.pom_enzyme_change, expected_pom_enzyme) assert np.allclose(mic_changes.maom_enzyme_change, expected_maom_enzyme) @@ -229,27 +232,36 @@ def test_calculate_nutrient_uptake_rates( calculate_nutrient_uptake_rates, ) - expected_carbon_gain = [8.06823526e-4, 2.78306304e-3, 4.66828324e-4, 2.77134516e-5] - expected_nitrogen_gain = [1.5515837e-4, 5.3520443e-4, 8.97746776e-5, 5.3295099e-6] - expected_carbon_consumption = [2.241176e-3, 8.433524e-3, 1.556094e-3, 5.7736357e-5] - - actual_carbon_gain, actual_carbon_consumption, actual_nitrogen_gain = ( - calculate_nutrient_uptake_rates( - soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"], - soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"], - soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], - 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, - ) + expected_carbon_gain = [2.48756225e-5, 1.57155834e-4, 4.66828319e-4, 2.12206561e-5] + expected_consumption_rates = { + "nitrogen": [4.78377356e-6, 3.02222758e-5, 8.97746767e-5, 4.08089540e-6], + "phosphorus": [1.55472641e-6, 9.82223962e-6, 2.91767699e-5, 1.32629101e-6], + "carbon": [6.90989514e-5, 4.76229800e-4, 1.55609440e-3, 4.42097002e-5], + } + + actual_carbon_gain, actual_consumption_rates = calculate_nutrient_uptake_rates( + soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"], + soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"], + soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"], + soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"], + 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, ) assert np.allclose(actual_carbon_gain, expected_carbon_gain) - assert np.allclose(actual_nitrogen_gain, expected_nitrogen_gain) - assert np.allclose(actual_carbon_consumption, expected_carbon_consumption) + + assert set(expected_consumption_rates.keys()) == set( + actual_consumption_rates.keys() + ) + + for key in expected_consumption_rates.keys(): + assert np.allclose( + expected_consumption_rates[key], actual_consumption_rates[key] + ) def test_calculate_highest_achievable_nutrient_uptake( diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index fea21f93e..c3c0169c1 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -289,20 +289,20 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.0558832, 0.02294602, 0.11309406, 0.01485682], dims="cell_id" + [0.05758271, 0.02723883, 0.11926384, 0.01489358], dims="cell_id" ), soil_c_pool_maom=DataArray( [2.5194618, 1.70483236, 4.53238116, 0.52968038], dims="cell_id" ), soil_c_pool_microbe=DataArray( - [5.77347072, 2.29002259, 11.24219185, 0.99642319], + [5.77285999, 2.28860898, 11.24034357, 0.99640556], dims="cell_id", ), soil_c_pool_pom=DataArray( [0.10088826, 0.99607827, 0.69401858, 0.35272508], dims="cell_id" ), soil_c_pool_necromass=DataArray( - [0.05840173, 0.01865113, 0.10631256, 0.06904725], dims="cell_id" + [0.05840079, 0.01864821, 0.10630987, 0.06904723], dims="cell_id" ), soil_enzyme_pom=DataArray( [0.02267842, 0.00957576, 0.05004963, 0.00300993], dims="cell_id" @@ -311,19 +311,19 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.0354453, 0.01167442, 0.02538637, 0.00454144], dims="cell_id" ), soil_n_pool_don=DataArray( - [0.00124825, 0.00320679, 0.00236633, 0.00388298], dims="cell_id" + [0.00136589, 0.00347904, 0.00272114, 0.00388636], dims="cell_id" ), soil_n_pool_particulate=DataArray( [0.00714836, 0.00074629, 0.00292269, 0.01429302], dims="cell_id" ), soil_n_pool_necromass=DataArray( - [0.00602181, 0.01303618, 0.02189848, 0.00758444], dims="cell_id" + [0.00602163, 0.01303562, 0.02189796, 0.00758443], dims="cell_id" ), soil_n_pool_maom=DataArray( [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), soil_p_pool_dop=DataArray( - [5.76474498e-6, 2.41003861e-5, 5.75967069e-5, 1.13124345e-4], + [5.03637699e-6, 1.95693195e-5, 4.22880874e-5, 1.12471095e-4], dims="cell_id", ), soil_p_pool_particulate=DataArray( @@ -331,7 +331,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): dims="cell_id", ), soil_p_pool_necromass=DataArray( - [0.00251446, 0.00083368, 0.00444109, 0.00067262], dims="cell_id" + [0.00251438, 0.00083347, 0.00444088, 0.00067262], dims="cell_id" ), soil_p_pool_maom=DataArray( [0.01307692, 0.03461538, 0.01923077, 0.00384615], dims="cell_id" @@ -455,18 +455,18 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): ) delta_pools = [ - 0.0129365, - 0.0060499, + 0.01510858, + 0.01400719, 0.03697928, - 0.02425546, + 0.02426899, 0.038767651, 0.00829848, 0.05982197, 0.07277182, - -0.05362396, - -0.02020101, + -0.0544059, + -0.02282691, -0.11965575, - -0.00719517, + -0.00720166, 0.00177803841, -0.007860960795, -0.012016245, @@ -483,10 +483,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -5.09593e-5, 0.0005990658, -3.72112e-5, - 0.00104884, - 0.00419763, + 0.00119921, + 0.00470261, 0.00496839, - 0.00251317, + 0.00251442, 1.102338e-5, 6.422491e-5, 0.000131687, @@ -499,10 +499,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - 9.53691781e-8, - 2.49063642e-6, - 9.08802987e-7, - -2.3349143e-6, + -1.4593572e-6, + -7.3316032e-6, + -2.8267967e-5, + -3.6612053e-6, 7.22218e-6, -1.13464e-6, 7.86083e-7, diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index b4a99c687..004e23711 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -341,7 +341,7 @@ class SoilConsts(ConstantsDataclass): """ max_uptake_rate_don = 0.0077 - """Maximum rate at for dissolved organic nitrogen uptake [day^-1]. + """Maximum possible rate for dissolved organic nitrogen uptake [day^-1]. This rate corresponds to the reference temperature given by :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by @@ -361,3 +361,25 @@ class SoilConsts(ConstantsDataclass): TODO - At present I've invented the value for this constant, so it really needs to be better pinned down. """ + + max_uptake_rate_dop = 0.0025 + """Maximum possible rate for dissolved organic 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_dop_uptake: float = 0.02275 + """Half saturation constant for uptake of dissolved organic phosphorus (DOP). + + [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. + """ diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 83a4a2fa9..30636f3c0 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -38,6 +38,11 @@ class MicrobialChanges: Units of [kg N m^-3 day^-1].""" + dop_uptake: NDArray[np.float32] + """Total rate of microbial uptake of dissolved organic phosphorus. + + Units of [kg P m^-3 day^-1].""" + microbe_change: NDArray[np.float32] """Rate of change of microbial biomass pool [kg C m^-3 day^-1].""" @@ -144,10 +149,10 @@ def calculate_all_pool_updates( constants=self.constants, ) # find changes related to microbial uptake, growth and decay - # TODO - Add phosphorus changes in here microbial_changes = calculate_microbial_changes( soil_c_pool_lmwc=self.pools["soil_c_pool_lmwc"], soil_n_pool_don=self.pools["soil_n_pool_don"], + soil_p_pool_dop=self.pools["soil_p_pool_dop"], soil_c_pool_microbe=self.pools["soil_c_pool_microbe"], soil_enzyme_pom=self.pools["soil_enzyme_pom"], soil_enzyme_maom=self.pools["soil_enzyme_maom"], @@ -305,6 +310,7 @@ def calculate_all_pool_updates( delta_pools_ordered["soil_p_pool_dop"] = ( litter_mineralisation_fluxes_P["dissolved"] + pom_p_mineralisation + - microbial_changes.dop_uptake - dop_leaching ) delta_pools_ordered["soil_p_pool_particulate"] = ( @@ -324,6 +330,7 @@ def calculate_all_pool_updates( def calculate_microbial_changes( soil_c_pool_lmwc: NDArray[np.float32], soil_n_pool_don: NDArray[np.float32], + soil_p_pool_dop: NDArray[np.float32], soil_c_pool_microbe: NDArray[np.float32], soil_enzyme_pom: NDArray[np.float32], soil_enzyme_maom: NDArray[np.float32], @@ -341,6 +348,7 @@ def calculate_microbial_changes( Args: soil_c_pool_lmwc: Low molecular weight carbon pool [kg C m^-3] soil_n_pool_don: Dissolved organic nitrogen pool [kg N m^-3] + soil_p_pool_dop: Dissolved organic phosphorus pool [kg P m^-3] soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] soil_enzyme_pom: Amount of enzyme class which breaks down particulate organic matter [kg C m^-3] @@ -352,21 +360,20 @@ def calculate_microbial_changes( constants: Set of constants for the soil model. Returns: - A dataclass containing the rate at which microbes uptake LMWC, the rate of - change in the microbial biomass pool and the enzyme pools. + A dataclass containing the rate at which microbes uptake LMWC, DON and DOP, and + the rate of change in the microbial biomass pool and the enzyme pools. """ # Calculate uptake, growth rate, and loss rate - biomass_growth, microbial_C_uptake, microbial_N_uptake = ( - calculate_nutrient_uptake_rates( - soil_c_pool_lmwc=soil_c_pool_lmwc, - soil_n_pool_don=soil_n_pool_don, - soil_c_pool_microbe=soil_c_pool_microbe, - water_factor=env_factors.water, - pH_factor=env_factors.pH, - soil_temp=soil_temp, - constants=constants, - ) + biomass_growth, microbial_uptake = calculate_nutrient_uptake_rates( + soil_c_pool_lmwc=soil_c_pool_lmwc, + soil_n_pool_don=soil_n_pool_don, + soil_p_pool_dop=soil_p_pool_dop, + soil_c_pool_microbe=soil_c_pool_microbe, + water_factor=env_factors.water, + pH_factor=env_factors.pH, + soil_temp=soil_temp, + constants=constants, ) biomass_loss = calculate_maintenance_biomass_synthesis( soil_c_pool_microbe=soil_c_pool_microbe, @@ -389,8 +396,9 @@ def calculate_microbial_changes( ) * biomass_loss return MicrobialChanges( - lmwc_uptake=microbial_C_uptake, - don_uptake=microbial_N_uptake, + lmwc_uptake=microbial_uptake["carbon"], + don_uptake=microbial_uptake["nitrogen"], + dop_uptake=microbial_uptake["phosphorus"], microbe_change=biomass_growth - biomass_loss, pom_enzyme_change=pom_enzyme_net_change, maom_enzyme_change=maom_enzyme_net_change, @@ -575,12 +583,13 @@ def calculate_enzyme_turnover( def calculate_nutrient_uptake_rates( soil_c_pool_lmwc: NDArray[np.float32], soil_n_pool_don: NDArray[np.float32], + soil_p_pool_dop: NDArray[np.float32], soil_c_pool_microbe: NDArray[np.float32], water_factor: NDArray[np.float32], pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], constants: SoilConsts, -) -> tuple[NDArray[np.float32], NDArray[np.float32], NDArray[np.float32]]: +) -> tuple[NDArray[np.float32], dict[str, NDArray[np.float32]]]: """Calculate the rate at which microbes uptake each nutrient. These rates are found based on the assumption that microbial stochiometry is @@ -597,6 +606,7 @@ def calculate_nutrient_uptake_rates( Args: soil_c_pool_lmwc: Low molecular weight carbon pool [kg C m^-3] soil_n_pool_don: Dissolved organic nitrogen pool [kg N m^-3] + soil_p_pool_dop: Dissolved organic phosphorus pool [kg P m^-3] soil_c_pool_microbe: Microbial biomass (carbon) pool [kg C m^-3] water_factor: A factor capturing the impact of soil water potential on microbial rates [unitless] @@ -607,7 +617,8 @@ def calculate_nutrient_uptake_rates( Returns: A tuple containing the rate at which microbial biomass increases due to nutrient - uptake, and the rate at which carbon and nitrogen get taken up. + uptake, as well as a dictionary containing the rate at which carbon, nitrogen + and phosphorus get taken up. """ # Calculate carbon use efficiency @@ -639,23 +650,40 @@ def calculate_nutrient_uptake_rates( half_saturation_constant=constants.half_sat_don_uptake, constants=constants, ) + phosphorus_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( + labile_nutrient_pool=soil_p_pool_dop, + soil_c_pool_microbe=soil_c_pool_microbe, + water_factor=water_factor, + pH_factor=pH_factor, + soil_temp=soil_temp, + max_uptake_rate=constants.max_uptake_rate_dop, + half_saturation_constant=constants.half_sat_dop_uptake, + constants=constants, + ) # Use carbon use efficency to determine maximum possible rate of carbon gain carbon_gain_max = carbon_uptake_rate_max * carbon_use_efficency # Find actual rate of carbon gain based on most limiting uptake rate, then find # nutrient gain and total carbon consumption based on this - actual_carbon_gain = np.minimum( - carbon_gain_max, constants.microbial_c_n_ratio * nitrogen_uptake_rate_max + actual_carbon_gain = np.minimum.reduce( + [ + carbon_gain_max, + constants.microbial_c_n_ratio * nitrogen_uptake_rate_max, + constants.microbial_c_p_ratio * phosphorus_uptake_rate_max, + ] ) - nitrogen_consumption_rate = actual_carbon_gain / constants.microbial_c_n_ratio - carbon_consumption_rate = actual_carbon_gain / carbon_use_efficency + consumption_rates = { + "nitrogen": actual_carbon_gain / constants.microbial_c_n_ratio, + "phosphorus": actual_carbon_gain / constants.microbial_c_p_ratio, + "carbon": actual_carbon_gain / carbon_use_efficency, + } # TODO - the quantities calculated above can be used to calculate the carbon # respired instead of being uptaken. This isn't currently of interest, but will be # in future - return actual_carbon_gain, carbon_consumption_rate, nitrogen_consumption_rate + return actual_carbon_gain, consumption_rates def calculate_highest_achievable_nutrient_uptake( From 6d8507f299269e95c752bf8b800b739586c8f5be Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Mon, 6 Jan 2025 11:20:39 +0000 Subject: [PATCH 7/8] Added tracking of phosphorus associated with microbial necromass decay/sorption --- tests/models/soil/test_pools.py | 31 ++++++++------ tests/models/soil/test_soil_model.py | 36 ++++++++-------- virtual_ecosystem/models/soil/pools.py | 59 ++++++++++++++++---------- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 036980673..f12a7544c 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -47,10 +47,10 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_p_pool_dop": [-1.4593572e-6, -7.3316032e-6, -2.8267967e-5, -3.6612053e-6], + "soil_p_pool_dop": [1.85156658e-4, 1.93268373e-5, 1.37019910e-4, 9.94213328e-5], "soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7], - "soil_p_pool_necromass": [0.0034213, 0.00143969, 0.00747022, 0.00045376], - "soil_p_pool_maom": [0.0, 0.0, 0.0, 0.0], + "soil_p_pool_necromass": [2.674836e-3, 1.333056e-3, 6.8090685e-3, 4.1429847e-5], + "soil_p_pool_maom": [5.59848046e-4, 7.99753217e-5, 4.9586363e-4, 3.09247615e-4], } # Make order of pools object @@ -437,24 +437,31 @@ def test_calculate_nutrient_flows_to_necromass(microbial_changes): assert np.allclose(actual_p_flow_to_necromass, expected_p_flow_to_necromass) -def test_find_necromass_nitrogen_outflows( +def test_find_necromass_nutrient_outflows( dummy_carbon_data, necromass_breakdown, necromass_sorption ): - """Test that function to find necromass nitrogen losses works correctly.""" - from virtual_ecosystem.models.soil.pools import find_necromass_nitrogen_outflows - - expected_decay = [0.00066649, 0.00413222, 0.00466541, 0.00257709] - expected_sorption = [0.00199947, 0.01239667, 0.01399624, 0.00773126] + """Test that function to find necromass nutrient losses works correctly.""" + from virtual_ecosystem.models.soil.pools import find_necromass_nutrient_outflows + + expected_rates = { + "decay_nitrogen": [0.00066649, 0.00413222, 0.00466541, 0.00257709], + "sorption_nitrogen": [0.00199947, 0.01239667, 0.01399624, 0.00773126], + "decay_phosphorus": [1.86616016e-4, 2.6658441e-5, 1.65287877e-4, 1.03082538e-4], + "sorption_phosphorus": [5.5984805e-4, 7.9975322e-5, 4.958636e-4, 3.0924762e-4], + } - actual_decay, actual_sorption = find_necromass_nitrogen_outflows( + actual_rates = find_necromass_nutrient_outflows( necromass_carbon=dummy_carbon_data["soil_c_pool_necromass"], necromass_nitrogen=dummy_carbon_data["soil_n_pool_necromass"], + necromass_phosphorus=dummy_carbon_data["soil_p_pool_necromass"], necromass_decay=necromass_breakdown, necromass_sorption=necromass_sorption, ) - assert np.allclose(actual_decay, expected_decay) - assert np.allclose(actual_sorption, expected_sorption) + assert set(expected_rates.keys()) == set(actual_rates.keys()) + + for key in expected_rates.keys(): + assert np.allclose(expected_rates[key], actual_rates[key]) def test_calculate_net_nitrogen_transfer_from_maom_to_don( diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index c3c0169c1..c8f9d4dcb 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -289,13 +289,13 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.05758271, 0.02723883, 0.11926384, 0.01489358], dims="cell_id" + [0.05718068, 0.02706181, 0.11774654, 0.01488859], dims="cell_id" ), soil_c_pool_maom=DataArray( [2.5194618, 1.70483236, 4.53238116, 0.52968038], dims="cell_id" ), soil_c_pool_microbe=DataArray( - [5.77285999, 2.28860898, 11.24034357, 0.99640556], + [5.77300454, 2.28866733, 11.24079819, 0.99640795], dims="cell_id", ), soil_c_pool_pom=DataArray( @@ -311,7 +311,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.0354453, 0.01167442, 0.02538637, 0.00454144], dims="cell_id" ), soil_n_pool_don=DataArray( - [0.00136589, 0.00347904, 0.00272114, 0.00388636], dims="cell_id" + [0.00133806, 0.00346781, 0.00263385, 0.0038859], dims="cell_id" ), soil_n_pool_particulate=DataArray( [0.00714836, 0.00074629, 0.00292269, 0.01429302], dims="cell_id" @@ -323,7 +323,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), soil_p_pool_dop=DataArray( - [5.03637699e-6, 1.95693195e-5, 4.22880874e-5, 1.12471095e-4], + [1.55757033e-4, 6.23612823e-5, 2.64802796e-4, 1.64607083e-4], dims="cell_id", ), soil_p_pool_particulate=DataArray( @@ -331,10 +331,10 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): dims="cell_id", ), soil_p_pool_necromass=DataArray( - [0.00251438, 0.00083347, 0.00444088, 0.00067262], dims="cell_id" + [0.00187526, 0.00064761, 0.00343342, 0.00046239], dims="cell_id" ), soil_p_pool_maom=DataArray( - [0.01307692, 0.03461538, 0.01923077, 0.00384615], dims="cell_id" + [0.01355627, 0.03475478, 0.0199864, 0.00400383], dims="cell_id" ), ) ), @@ -499,22 +499,22 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - -1.4593572e-6, - -7.3316032e-6, - -2.8267967e-5, - -3.6612053e-6, + 1.85156658e-4, + 1.93268373e-5, + 1.37019910e-4, + 9.94213328e-5, 7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7, - 0.0034213, - 0.00143969, - 0.00747022, - 0.00045376, - 0.0, - 0.0, - 0.0, - 0.0, + 2.674836e-3, + 1.333056e-3, + 6.8090685e-3, + 4.1429847e-5, + 5.59848046e-4, + 7.99753217e-5, + 4.9586363e-4, + 3.09247615e-4, ] # make pools diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 30636f3c0..c69b67f26 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -242,9 +242,10 @@ def calculate_all_pool_updates( microbial_changes=microbial_changes, constants=self.constants ) # Find nitrogen released by necromass breakdown/sorption - necromass_n_decay, necromass_n_sorption = find_necromass_nitrogen_outflows( + necromass_outflows = find_necromass_nutrient_outflows( necromass_carbon=self.pools["soil_c_pool_necromass"], necromass_nitrogen=self.pools["soil_n_pool_necromass"], + necromass_phosphorus=self.pools["soil_p_pool_necromass"], necromass_decay=necromass_decay_to_lmwc, necromass_sorption=necromass_sorption_to_maom, ) @@ -292,7 +293,7 @@ def calculate_all_pool_updates( delta_pools_ordered["soil_n_pool_don"] = ( litter_mineralisation_fluxes_N["dissolved"] + pom_n_mineralisation - + necromass_n_decay + + necromass_outflows["decay_nitrogen"] + nitrogen_transfer_maom_to_don - microbial_changes.don_uptake - don_leaching @@ -302,24 +303,31 @@ def calculate_all_pool_updates( litter_mineralisation_fluxes_N["particulate"] - pom_n_mineralisation ) delta_pools_ordered["soil_n_pool_necromass"] = ( - necromass_n_flow - necromass_n_decay - necromass_n_sorption + necromass_n_flow + - necromass_outflows["decay_nitrogen"] + - necromass_outflows["sorption_nitrogen"] ) delta_pools_ordered["soil_n_pool_maom"] = ( - necromass_n_sorption - nitrogen_transfer_maom_to_don + necromass_outflows["sorption_nitrogen"] - nitrogen_transfer_maom_to_don ) delta_pools_ordered["soil_p_pool_dop"] = ( litter_mineralisation_fluxes_P["dissolved"] + pom_p_mineralisation + + necromass_outflows["decay_phosphorus"] - microbial_changes.dop_uptake - dop_leaching ) delta_pools_ordered["soil_p_pool_particulate"] = ( litter_mineralisation_fluxes_P["particulate"] - pom_p_mineralisation ) - delta_pools_ordered["soil_p_pool_necromass"] = necromass_p_flow - delta_pools_ordered["soil_p_pool_maom"] = np.zeros_like( - delta_pools_ordered["soil_n_pool_maom"] + delta_pools_ordered["soil_p_pool_necromass"] = ( + necromass_p_flow + - necromass_outflows["decay_phosphorus"] + - necromass_outflows["sorption_phosphorus"] ) + delta_pools_ordered["soil_p_pool_maom"] = necromass_outflows[ + "sorption_phosphorus" + ] delta_pools_ordered["soil_enzyme_pom"] = microbial_changes.pom_enzyme_change delta_pools_ordered["soil_enzyme_maom"] = microbial_changes.maom_enzyme_change @@ -963,42 +971,49 @@ def calculate_nutrient_flows_to_necromass( ) -def find_necromass_nitrogen_outflows( +def find_necromass_nutrient_outflows( necromass_carbon: NDArray[np.float32], necromass_nitrogen: NDArray[np.float32], + necromass_phosphorus: NDArray[np.float32], necromass_decay: NDArray[np.float32], necromass_sorption: NDArray[np.float32], -) -> tuple[NDArray[np.float32], NDArray[np.float32]]: - """Find the amount of nitrogen flowing out of the necromass pool. +) -> dict[str, NDArray[np.float32]]: + """Find the amount of each nutrient flowing out of the necromass pool. There are two sources for this outflow. Firstly, the decay of necromass to dissolved - organic nitrogen. Secondly, the sorption of necromass to soil minerals to form - mineral associated organic matter. A key assumption here is that nitrogen flow - directly follows the carbon flow, i.e. it follows the same split between pathways as - the carbon does. + organic nitrogen/phosphorus. Secondly, the sorption of necromass to soil minerals to + form mineral associated organic matter. A key assumption here is that the nitrogen + and phosphorus flows directly follows the carbon flow, i.e. it follows the same + split between pathways as the carbon does. Args: necromass_carbon: The amount of carbon stored as microbial necromass [kg C m^-3] necromass_nitrogen: The amount of nitrogen stored as microbial necromass [kg N m^-3] + necromass_phosphorus: The amount of phosphorus stored as microbial necromass [kg + P m^-3] necromass_decay: The rate at which necromass decays to form lmwc [kg C m^-3 day^-1] necromass_sorption: The rate at which necromass gets sorbed to soil minerals to form mineral associated organic matter [kg C m^-3 day^-1] Returns: - A tuple containing the rate at which nitrogen contained in necromass is released - as dissolved organic nitrogen, and the rate at which it gets sorbed to soil - minerals to form soil associated organic matter [kg N m^-3 day^-1]. + A dictionary containing the rates at which nitrogen and phosphrous contained in + necromass is released as dissolved organic nitrogen, and the rates at which they + gets sorbed to soil minerals to form soil associated organic matter [kg nutrient + m^-3 day^-1]. """ - # Find carbon:nitrogen ratio of the necromass + # Find carbon:nitrogen and carbon:phosphorus ratios of the necromass c_n_ratio = necromass_carbon / necromass_nitrogen + c_p_ratio = necromass_carbon / necromass_phosphorus - decay_nitrogen = necromass_decay / c_n_ratio - sorption_nitrogen = necromass_sorption / c_n_ratio - - return decay_nitrogen, sorption_nitrogen + return { + "decay_nitrogen": necromass_decay / c_n_ratio, + "sorption_nitrogen": necromass_sorption / c_n_ratio, + "decay_phosphorus": necromass_decay / c_p_ratio, + "sorption_phosphorus": necromass_sorption / c_p_ratio, + } def calculate_net_nitrogen_transfer_from_maom_to_don( From 54bbb2da769b859688de25b297ad6376ab805f47 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Mon, 6 Jan 2025 12:56:57 +0000 Subject: [PATCH 8/8] Added phosphorus component to LMWC to MAOM flow --- tests/models/soil/test_pools.py | 22 ++++++++---- tests/models/soil/test_soil_model.py | 26 +++++++------- virtual_ecosystem/models/soil/pools.py | 49 ++++++++++++++++++-------- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index f12a7544c..59ace5586 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -47,10 +47,10 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): "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_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315], - "soil_p_pool_dop": [1.85156658e-4, 1.93268373e-5, 1.37019910e-4, 9.94213328e-5], + "soil_p_pool_dop": [1.92918032e-4, 6.24454858e-5, 1.57222238e-4, 9.94118894e-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_maom": [5.59848046e-4, 7.99753217e-5, 4.9586363e-4, 3.09247615e-4], + "soil_p_pool_maom": [5.52086672e-4, 3.68566732e-5, 4.7566130e-4, 3.09257058e-4], } # Make order of pools object @@ -464,24 +464,32 @@ def test_find_necromass_nutrient_outflows( assert np.allclose(expected_rates[key], actual_rates[key]) -def test_calculate_net_nitrogen_transfer_from_maom_to_don( +def test_calculate_net_nutrient_transfers_from_maom_to_lmwc( dummy_carbon_data, enzyme_mediated_rates, lmwc_sorption, maom_desorption ): """Test function to find net exchange of nitrogen between maom and don.""" from virtual_ecosystem.models.soil.pools import ( - calculate_net_nitrogen_transfer_from_maom_to_don, + calculate_net_nutrient_transfers_from_maom_to_lmwc, ) - expected_transfer = [5.13427177e-4, 5.97759087e-4, 3.44268148e-4, -2.36081562e-7] + expected_transfers = { + "nitrogen": [5.13427177e-4, 5.97759087e-4, 3.44268148e-4, -2.36081562e-7], + "phosphorus": [7.76137416e-6, 4.31186485e-5, 2.02023283e-5, -9.44337153e-9], + } - actual_transfer = calculate_net_nitrogen_transfer_from_maom_to_don( + actual_transfers = calculate_net_nutrient_transfers_from_maom_to_lmwc( lmwc_carbon=dummy_carbon_data["soil_c_pool_lmwc"], lmwc_nitrogen=dummy_carbon_data["soil_n_pool_don"], + lmwc_phosphorus=dummy_carbon_data["soil_p_pool_dop"], maom_carbon=dummy_carbon_data["soil_c_pool_maom"], maom_nitrogen=dummy_carbon_data["soil_n_pool_maom"], + maom_phosphorus=dummy_carbon_data["soil_p_pool_maom"], maom_breakdown=enzyme_mediated_rates.maom_to_lmwc, maom_desorption=maom_desorption, lmwc_sorption=lmwc_sorption, ) - assert np.allclose(actual_transfer, expected_transfer) + assert set(expected_transfers.keys()) == set(actual_transfers.keys()) + + for key in expected_transfers.keys(): + assert np.allclose(expected_transfers[key], actual_transfers[key]) diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index c8f9d4dcb..2b292e283 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -289,13 +289,13 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.05718068, 0.02706181, 0.11774654, 0.01488859], dims="cell_id" + [0.0571695, 0.02695769, 0.11767019, 0.01488859], dims="cell_id" ), soil_c_pool_maom=DataArray( [2.5194618, 1.70483236, 4.53238116, 0.52968038], dims="cell_id" ), soil_c_pool_microbe=DataArray( - [5.77300454, 2.28866733, 11.24079819, 0.99640795], + [5.77300855, 2.28870164, 11.24082106, 0.99640795], dims="cell_id", ), soil_c_pool_pom=DataArray( @@ -311,7 +311,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.0354453, 0.01167442, 0.02538637, 0.00454144], dims="cell_id" ), soil_n_pool_don=DataArray( - [0.00133806, 0.00346781, 0.00263385, 0.0038859], dims="cell_id" + [0.00133728, 0.00346121, 0.00262946, 0.0038859], dims="cell_id" ), soil_n_pool_particulate=DataArray( [0.00714836, 0.00074629, 0.00292269, 0.01429302], dims="cell_id" @@ -323,7 +323,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.86671423, 0.48576345, 0.33406677, 0.09935391], dims="cell_id" ), soil_p_pool_dop=DataArray( - [1.55757033e-4, 6.23612823e-5, 2.64802796e-4, 1.64607083e-4], + [1.59404568e-4, 8.17510225e-5, 2.73598058e-4, 1.64590927e-4], dims="cell_id", ), soil_p_pool_particulate=DataArray( @@ -334,7 +334,7 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): [0.00187526, 0.00064761, 0.00343342, 0.00046239], dims="cell_id" ), soil_p_pool_maom=DataArray( - [0.01355627, 0.03475478, 0.0199864, 0.00400383], dims="cell_id" + [0.01355237, 0.03473323, 0.01997613, 0.00400384], dims="cell_id" ), ) ), @@ -499,10 +499,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 0.01179891, 0.01365197, 0.0077315, - 1.85156658e-4, - 1.93268373e-5, - 1.37019910e-4, - 9.94213328e-5, + 1.92918032e-4, + 6.24454858e-5, + 1.57222238e-4, + 9.94118894e-5, 7.22218e-6, -1.13464e-6, 7.86083e-7, @@ -511,10 +511,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): 1.333056e-3, 6.8090685e-3, 4.1429847e-5, - 5.59848046e-4, - 7.99753217e-5, - 4.9586363e-4, - 3.09247615e-4, + 5.52086672e-4, + 3.68566732e-5, + 4.7566130e-4, + 3.09257058e-4, ] # make pools diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index c69b67f26..394819306 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -250,12 +250,14 @@ def calculate_all_pool_updates( necromass_sorption=necromass_sorption_to_maom, ) # Find net nitrogen transfer between maom and lmwc/don - nitrogen_transfer_maom_to_don = ( - calculate_net_nitrogen_transfer_from_maom_to_don( + nutrient_transfers_maom_to_lmwc = ( + calculate_net_nutrient_transfers_from_maom_to_lmwc( lmwc_carbon=self.pools["soil_c_pool_lmwc"], lmwc_nitrogen=self.pools["soil_n_pool_don"], + lmwc_phosphorus=self.pools["soil_p_pool_dop"], maom_carbon=self.pools["soil_c_pool_maom"], maom_nitrogen=self.pools["soil_n_pool_maom"], + maom_phosphorus=self.pools["soil_p_pool_maom"], maom_breakdown=enzyme_mediated.maom_to_lmwc, maom_desorption=maom_desorption_to_lmwc, lmwc_sorption=lmwc_sorption_to_maom, @@ -294,7 +296,7 @@ def calculate_all_pool_updates( litter_mineralisation_fluxes_N["dissolved"] + pom_n_mineralisation + necromass_outflows["decay_nitrogen"] - + nitrogen_transfer_maom_to_don + + nutrient_transfers_maom_to_lmwc["nitrogen"] - microbial_changes.don_uptake - don_leaching ) @@ -308,12 +310,14 @@ def calculate_all_pool_updates( - necromass_outflows["sorption_nitrogen"] ) delta_pools_ordered["soil_n_pool_maom"] = ( - necromass_outflows["sorption_nitrogen"] - nitrogen_transfer_maom_to_don + necromass_outflows["sorption_nitrogen"] + - nutrient_transfers_maom_to_lmwc["nitrogen"] ) delta_pools_ordered["soil_p_pool_dop"] = ( litter_mineralisation_fluxes_P["dissolved"] + pom_p_mineralisation + necromass_outflows["decay_phosphorus"] + + nutrient_transfers_maom_to_lmwc["phosphorus"] - microbial_changes.dop_uptake - dop_leaching ) @@ -325,9 +329,10 @@ def calculate_all_pool_updates( - necromass_outflows["decay_phosphorus"] - necromass_outflows["sorption_phosphorus"] ) - delta_pools_ordered["soil_p_pool_maom"] = necromass_outflows[ - "sorption_phosphorus" - ] + delta_pools_ordered["soil_p_pool_maom"] = ( + necromass_outflows["sorption_phosphorus"] + - nutrient_transfers_maom_to_lmwc["phosphorus"] + ) delta_pools_ordered["soil_enzyme_pom"] = microbial_changes.pom_enzyme_change delta_pools_ordered["soil_enzyme_maom"] = microbial_changes.maom_enzyme_change @@ -1016,26 +1021,32 @@ def find_necromass_nutrient_outflows( } -def calculate_net_nitrogen_transfer_from_maom_to_don( +def calculate_net_nutrient_transfers_from_maom_to_lmwc( lmwc_carbon: NDArray[np.float32], lmwc_nitrogen: NDArray[np.float32], + lmwc_phosphorus: NDArray[np.float32], maom_carbon: NDArray[np.float32], maom_nitrogen: NDArray[np.float32], + maom_phosphorus: NDArray[np.float32], maom_breakdown: NDArray[np.float32], maom_desorption: NDArray[np.float32], lmwc_sorption: NDArray[np.float32], -) -> NDArray[np.float32]: - """Calculate the net rate of transfer of nitrogen between MAOM and LMWC/DON. +) -> dict[str, NDArray[np.float32]]: + """Calculate the net rate of transfer of nutrients between MAOM and LMWC. Args: lmwc_carbon: The amount of carbon stored as low molecular weight carbon [kg C m^-3] lmwc_nitrogen: The amount of nitrogen stored as low molecular weight carbon/dissolved organic nitrogen [kg N m^-3] + lmwc_phosphorus: The amount of phosphorus stored as low molecular weight + carbon/dissolved organic phosphorus [kg P m^-3] maom_carbon: The amount of carbon stored as mineral associated organic matter [kg C m^-3] maom_nitrogen: The amount of nitrogen stored as mineral associated organic matter [kg N m^-3] + maom_phosphorus: The amount of phosphorus stored as mineral associated organic + matter [kg P m^-3] maom_breakdown: The rate at which the mineral associated organic matter pool is being broken down by enzymes (expressed in carbon terms) [kg C m^-3 day^-1] maom_desorption: The rate at which the mineral associated organic matter pool is @@ -1044,9 +1055,9 @@ def calculate_net_nitrogen_transfer_from_maom_to_don( to minerals to form mineral associated organic matter [kg C m^-3 day^-1] Returns: - The net rate of transfer from nitrogen contained in mineral associated - organic matter and nitrogen existing as dissolved organic nitrogen [kg N m^-3 - day^-1] + The net nutrient transfer rates of transfer from mineral associated organic + matter into dissolved organic forms. This is currently includes nitrogen and + phosphorus [kg nutrient m^-3 day^-1] """ # Find carbon:nitrogen ratio of the lwmc and maom @@ -1056,4 +1067,14 @@ def calculate_net_nitrogen_transfer_from_maom_to_don( maom_nitrogen_gain = lmwc_sorption / c_n_ratio_lmwc maom_nitrogen_loss = (maom_breakdown + maom_desorption) / c_n_ratio_maom - return maom_nitrogen_loss - maom_nitrogen_gain + # Find carbon:phosphorus ratio of the lwmc and maom + c_p_ratio_lmwc = lmwc_carbon / lmwc_phosphorus + c_p_ratio_maom = maom_carbon / maom_phosphorus + + maom_phosphorus_gain = lmwc_sorption / c_p_ratio_lmwc + maom_phosphorus_loss = (maom_breakdown + maom_desorption) / c_p_ratio_maom + + return { + "nitrogen": maom_nitrogen_loss - maom_nitrogen_gain, + "phosphorus": maom_phosphorus_loss - maom_phosphorus_gain, + }