From 7cf803f5ff1bee919da3a0a331d6f62ab45e6f0a Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Mon, 24 Feb 2025 09:58:46 +0000 Subject: [PATCH 1/8] Split microbial pool up into a bacterial and a fungal pool --- tests/conftest.py | 3 ++- tests/core/test_data.py | 3 ++- tests/models/soil/conftest.py | 2 +- tests/models/soil/test_pools.py | 11 ++++++----- tests/models/soil/test_soil_model.py | 13 +++++++++++-- tests/test_main.py | 3 ++- virtual_ecosystem/data_variables.toml | 11 +++++++++-- .../example_data/config/data_config.toml | 5 ++++- .../example_data/data/example_soil_data.nc | Bin 36697 -> 39485 bytes .../generation_scripts/soil_example_data.py | 11 ++++++++--- virtual_ecosystem/models/soil/pools.py | 15 ++++++++++----- virtual_ecosystem/models/soil/soil_model.py | 9 ++++++--- 12 files changed, 61 insertions(+), 25 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 67055ae36..2b175bef1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -288,7 +288,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 450341b20..6eb2e3c0e 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..c51e64ca1 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -120,7 +120,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_microbe=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"][ diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 8ba12a457..ca770c263 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -44,7 +44,8 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components): change_in_pools = { "soil_c_pool_lmwc": [0.014984117633, 0.0133384581, 0.03449812333, 0.02425546], "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.054361097, -0.022606231, -0.118911406, -0.007195167], "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], @@ -111,7 +112,7 @@ 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_microbe=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"][ @@ -226,7 +227,7 @@ 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"], + soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_bacteria"], soil_temp=dummy_carbon_data["soil_temperature"][ fixture_core_components.layer_structure.index_topsoil_scalar ], @@ -307,7 +308,7 @@ 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"], + soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_bacteria"], water_factor=environmental_factors.water, pH_factor=environmental_factors.pH, soil_temp=dummy_carbon_data["soil_temperature"][ @@ -341,7 +342,7 @@ 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"], + soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_bacteria"], water_factor=environmental_factors.water, pH_factor=environmental_factors.pH, soil_temp=dummy_carbon_data["soil_temperature"][ diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 46e15d164..066bf7321 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"), @@ -304,10 +305,14 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): soil_c_pool_maom=DataArray( [2.5194618, 1.70483236, 4.53238116, 0.52968038], dims="cell_id" ), - soil_c_pool_microbe=DataArray( + soil_c_pool_bacteria=DataArray( [5.77303027, 2.2888041, 11.24109943, 0.9964216], dims="cell_id", ), + soil_c_pool_fungi=DataArray( + [0.86302169, 8.53880051, 2.15105403, 4.5364215], + dims="cell_id", + ), soil_c_pool_pom=DataArray( [0.10088826, 0.99607827, 0.69401858, 0.35272508], dims="cell_id" ), @@ -496,6 +501,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -0.022606231, -0.118911406, -0.007195167, + -0.054361097, + -0.022606231, + -0.118911406, + -0.007195167, 0.00177803841, -0.007860960795, -0.012016245, diff --git a/tests/test_main.py b/tests/test_main.py index 07ab3c025..997282edd 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"), diff --git a/virtual_ecosystem/data_variables.toml b/virtual_ecosystem/data_variables.toml index 042faaad9..d6fd75b5d 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 7c4b4d436..d9e3cf10c 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/data/example_soil_data.nc b/virtual_ecosystem/example_data/data/example_soil_data.nc index 5109d789c76106f78041ff3187cf48d423bd4a9b..d6a0707d6f2227fb062d18f6b9d1ae079e8e7b4d 100644 GIT binary patch delta 4385 zcmaJ_3s6)?7VYrL?7#rQ9|i}oHdvyF zT(eqk;;NO3Q5RzNk+>miz!*2iXbif3vP+q@5@TYtvToL@q^#(6zkU}UPfb(B>pu6~ zd;4?y_O1D1pLn86Y_laL?IlW!;vcf>(Au=D%=E>gXO|eNU~V>5Wboph1P}i8nY=T< zUFx40*aw|+b9Vl4@EJlzs9eqGlT&-_@Ic6h-o^}Q6ITm1Xf#>jy=|$G=hkZcZae4R z24DA;24R}trm+6K>FuV~a*tFmSl*;kfiZkI?J)hsMBZHvv&0+_q$DYBctaC>Gd$Fo z)5;l&At~pT+lOJj`*Xo@#Gm2CrAcv-Ga_Rqtu8K|niL%w9Tyq*PH^LT>IfDY&X1lp znQIGfg#5xB3iAj#P6woWckOl9KkQY@5r#6BI+dfedwSg;GU1Lf_%{(uAV}`)L!1AL z&N4^g%EBtA(^0s>zOuHYrp#_sH`!{t*^`=^nNEfgkq{H57mrI$RP>0I{_!R*z5g!t zn+1L~QGwk*<^SX;tgt&Py48s?QrnSMHmi3|^y+p%P;g zG7YA91q)WXZxOK8D@3~gUGoLl>s2X-M4AO_CuL|&yrt;a*UVc6)4j`mdv)TI5YiAQ z_1VTuh1=fYE_P6?8Tg0n*~UBUz*s}PjV_rrwwtUWj(oPOtRaqkwm{YpM?PC9ZLR?U zDzN3SrdQz08pM%*jME?EzpcZO|Cb@Siwvw-9wMNpq^`J2hvN#~(c#E{SBE42xDHqN z)Ia&FPJjyU>2P$=t;3Q3z79wJ-*h9#LxqoYI6643!;$}Y9gh5ub$FY|{gX2~0V;f=!_mQ69gh4@bvW|R>2Tzq zSN%VB1fM-c;erlF2cPS3bkL{6k$+LeUHWTw=_v~T(BbIdvJOWFS9Cb?uMWlGd4FHA z$|P6&J52KM5DzaX^03$jv+35c4IAeyp*fcCXz%Y`jy9wP> zp`vcN&;`-Cep1I$xefZJ?2|X8(cH_DiPiwmAps$`X28DaQaGOO1FonFZyz6KDLIAx zC!`D7VgjT!yH9n3AT>4x=EwTTO|iX_e0FAzV6=O1=4OTIY#yEm$%z4;Z&O#H zWKMQPJZXiZM6;`drl?R7AupUyLKWGJh4&Yea{By@BjeW>6q0622q%|Z9(Hnpb|(<& z_^dMWkXnD1e70k{gTzyG9655mzKOI`wXr1eM$cAarf3+c_ubo0epb3!2U7NsbV>*( zGp|_=5tb9fNb<(~+D@{V_Jc`4lBt)l!;K(!j~dRAB-%BTvhl)YQbbKekm~&IZ%I0x zB1ns>1LGj%?!YyO*yDhIOO|l!67}yIJ#N1I^&I>50)8pb7{Fk0#Q0%8E$Ubc?%j# zs^P$}Ke|4o5MB5{;d6?1-M>s+_vA}6q;QfN@65?ePbFA8GArR}51fL8{)P2;qXh{k zA#A|gFGwPR{Mq|Uf3=_zB+Nd(+PbTBP35Yxm;~mNE;$^aE+46x(yX2w4|#D&qfADhL2)XWw0{sG>c~11PP;s8>!?aVX?EaHV+vXK%*2R za3yu5xHK2M)12}z8D;^pbEN?7_Vf3@=6~e}Sy}4K-ZZWFG^@*uZ;g0mGWojVxXkjD zye*sN7*-x=iM%X0XRh*7?69{^EB;ui@4jmMZ!-91n%(U&c~KJ%hr#Uld2)MvyeN0h z-@^aVOST#oI7{V;OQ;uCRA|lcVq!<s;+8TRp z$!m6G`N5{*QKow(IQEDA#lexWML5O;#b;N=W1Y?JW))Hb;BIkH^a3Z2F>7(hs!CN& zS%tl3!;Ly*Ssp%eV5#_?(*6Mg}i-ipCx@$p5Z#3Gy-r4fVnrRkN_J+`{TGt;Tse zJ|gV@Ua1N?RI?X#Bwi9-Y>9dVYhQ9c%Stn?4OWt!NGupLco81*i@-`IE%7oP6XLi$ zGZ1zPIpC^)1R<^r@ZT|8dU4kDW)}Izn=J~7$Z#N`PV8!gpu`|4=;F1nVDb)&T;Gr& zz(*-rQvR-2ZL)J6U7o3FV0oTU?YadwTXG<)rF~@Xr^aY)6_e1$su*8ZmuV+;m8A>M zLz&0ER(?M4%AmWyEhU)UXe&wB!)GJXIA+akBNF|%;ZFf)X;@!*W|x)u@$45!AZ*#b z$cQhPMK0gEPVmoHYh1-E)sC3Db`orF$$d=lyM~|Nmjj#sBgzf_bWw$Mp78QEpJ7ij Re0{sdFq;``_22H<{C`xA_sswR delta 4076 zcmaJ^3s6+o8UF9(0=q1a<*~3VA_xLXl()giL*yab5aeCOT9?NzvUHaP1mX)#%(dSW1dhpJILC3JDxK|IP4hMLm$;>Z29_m-oQ#BlWG#v~!C@{MA| z30cC}oth|g$0KU0M!~pqUO!snxg;VLf`|%ZLsnYC3kfN68jTI}vXT>$(-Klw?nOh` zA}Ur~n692n4RP)OkBDW?0nazK*X zQjlAO(*2tJKa>rhKs7yS!dJ(_)6h45vGze~1r(0w^F8#1Y%OUF?&Y#;tC8(;$O%W!5ul;Mm&BEuQ~0~yZvM=?(9KjqS6&k#5+ z!;#3KIOC5_;*x#m7K}{}NN-HwBN@(Ca8iad{wW#G_~SCXm#6mOv}}M0 zd@RG6fip6k@&6*j8UGU*&iJ1?>_6qAv(FIts|;rbK9k|hz&RPt_W$5v>i1;P9Jk(a;0xcp1w#eCG<)U^o#NpJA8y-S0+7w#i5;}=yKw2ez;Id zhuk4_%RA5m;6dJv}qIMH==Wpdv zf?kiZ^$M|5Kc5%REhyuZb&l#o>4{sjlTmhNuy+rhMHDP9way1Ks>#&YyK!!ef;jP| z7vV8B5Ct#1^spQXahaM0*<&}C!ZnP>!t6D_Hh>XxW8sS3t5&K8@o;f_x&<^CodpN~ z(X|C^SS$uIZ=LFf>)22+WkOId^D_DU)??o<@h}U z?jKT|hb;W60rPC`Dr8}~IB2ataTg-6wplPyb@&_jk7VwEuJhl}in)=n|COO1u?XO* zeDX)lPf#c=afE;O2@K=cc<>0>vkwgheTA~KSK4Ep;p$kg5=%;r+HiOo9BN1%m6{o= ziUx1irGE5bU24dAXQ7MtW24kVQaDIwhAzyX4O6!UurA$^K-ML7H=f=&2t=JpmMb;t zjbPTL{taPWx{X@a#oH5uf9x*caGH({&&Dr7aOAZ+I_xdG_hbyu*hpfS8%4>LBs!n` zrh_AA;(2-|4hoy^E!OAf0=w4{eft*{*g1h&;?}Su#|b)=zvzRA|96w&1O>{<4N;|J z(3x?95*c#C?b(*LL_)=uuRlpc*-_))QYpu;1TgoVGFR7AddZ{wn znm-}Yh#M!c?G2HXphvj@G3`}skZm@#;1DMkmRF)2TdQbG(s0vBJ;j0Jyxh?`>brWw zS>j#vb8;|RTD}C8R!&V1v^I=9{^eQXXC3u3TRxotz}MDeqLhm^=4m(`Da5XpXaWA+ z&W;Ocw5(A_s*3CIxTIw)vH$OHepiNK`=f>X*9TI?;TP5JC}WvkT)!-b$5GITifj$y z;{u%QNac3bKr8mHAJQWx10~2#0wwmejQX);tjoPeKS?!7RNWaFl@_6uZ8Nr^Q{@)1 zrxY)Z+yPuIliCk(acHcS^W*z>4M@rKaK1B{j^_{+_{J(F z(@S0U2xREj30J;o_Z3H%V;_9iu18PW72?~g=1YDMQ)>h_yk8#toI8%{x9dXS4=$>_#AIH=mk#jLvxJbDZ7&?nG>YMfIC4_m^mV;)@rpin>(z^rHtl} z)*HiA*Xme501X;L=Dl0X`YdD6Xl(bW3fW*<#wf2?tS| z_`NBWi+jeWg~jEg==1X%-V=Xi*~-&DLmMt_jv(ZZ+UX|)xh=SE@;PbNuk>Y{7Qbw< zbIzSh?-&tYqmwMAUD4nG=Q;Rr}D--DL{tbKEDy4eja4K@0n|O3gG7@TA<>cl0^;|E1*p MKCI-kNdMvg0?Z4|W&i*H 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/pools.py b/virtual_ecosystem/models/soil/pools.py index 9017c1fff..511df1872 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -195,8 +195,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].""" @@ -351,6 +354,7 @@ def calculate_all_pool_updates( clay_fraction=self.data["clay_fraction"].to_numpy(), constants=self.constants, ) + # TODO - This needs to be changed to accept both fungi and bacteria # find changes related to microbial uptake, growth and decay microbial_changes = calculate_microbial_changes( soil_c_pool_lmwc=self.pools.soil_c_pool_lmwc, @@ -359,7 +363,7 @@ 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_microbe=self.pools.soil_c_pool_bacteria, soil_enzyme_pom=self.pools.soil_enzyme_pom, soil_enzyme_maom=self.pools.soil_enzyme_maom, soil_temp=soil_temperature, @@ -542,8 +546,9 @@ 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 + # TODO - Need to not just feed the same change to both + delta_pools_ordered["soil_c_pool_bacteria"] = microbial_changes.microbe_change + delta_pools_ordered["soil_c_pool_fungi"] = microbial_changes.microbe_change delta_pools_ordered["soil_c_pool_pom"] = ( litter_mineralisation_flux.pom - enzyme_mediated.pom_to_lmwc ) diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index 22e31ad32..fded7af08 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -46,7 +46,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", @@ -72,7 +73,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", @@ -102,7 +104,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", From 79c383677e652c33421407dffe1a85e005cf5814 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Mon, 24 Feb 2025 11:04:07 +0000 Subject: [PATCH 2/8] Changed generic microbial constants to be bacterial specific (and updated the functions accordingly) --- tests/models/soil/conftest.py | 2 +- tests/models/soil/test_pools.py | 16 ++-- virtual_ecosystem/models/soil/constants.py | 62 +++++++------- virtual_ecosystem/models/soil/pools.py | 98 ++++++++++++---------- 4 files changed, 96 insertions(+), 82 deletions(-) diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index c51e64ca1..d8960ce8f 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -120,7 +120,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_bacteria"], + 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"][ diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index ca770c263..3220b9339 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -99,7 +99,7 @@ def test_calculate_microbial_changes( "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], + "bacteria_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], @@ -112,7 +112,7 @@ 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_bacteria"], + 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"][ @@ -227,11 +227,13 @@ 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_bacteria"], + 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_turnover_rate=SoilConsts.bacterial_turnover_rate, + activation_energy_turnover=SoilConsts.activation_energy_microbial_turnover, + reference_temperature=SoilConsts.arrhenius_reference_temp, ) assert np.allclose(actual_loss, expected_loss) @@ -308,7 +310,7 @@ 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_bacteria"], + soil_c_pool_bacteria=dummy_carbon_data["soil_c_pool_bacteria"], water_factor=environmental_factors.water, pH_factor=environmental_factors.pH, soil_temp=dummy_carbon_data["soil_temperature"][ @@ -348,8 +350,8 @@ def test_calculate_highest_achievable_nutrient_uptake( 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, + max_uptake_rate=SoilConsts.max_bacterial_uptake_rate_labile_C, + half_saturation_constant=SoilConsts.half_sat_bacterial_labile_C_uptake, constants=SoilConsts, ) diff --git a/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 77a7357ff..ae70f05e6 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -58,28 +58,28 @@ class SoilConsts(ConstantsDataclass): """ # TODO - Split this and the following into 2 constants once fungi are introduced - max_uptake_rate_labile_C: float = 0.04 + max_bacterial_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`. + 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]. + """Activation energy for bacterial 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. + :attr:`max_bacterial_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). + half_sat_bacterial_labile_C_uptake: float = 0.364 + """Half saturation constant for bacterial 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]. @@ -158,8 +158,8 @@ class SoilConsts(ConstantsDataclass): """ # 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]. + bacterial_turnover_rate: float = 0.005 + """Bacterial 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 @@ -172,8 +172,8 @@ class SoilConsts(ConstantsDataclass): 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`. + Value taken from :cite:t:`wang_development_2013`. The bacterial turnover rate that + this activation energy corresponds to is given by :attr:`bacterial_turnover_rate`. """ # TODO - At some point I need to split these enzyme constants into fungi and @@ -202,8 +202,8 @@ class SoilConsts(ConstantsDataclass): [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,20 +347,18 @@ class SoilConsts(ConstantsDataclass): leaches from litter solely in organic form. """ - microbial_c_n_ratio = 5.2 - """Ratio of carbon to nitrogen in microbial biomass [unitless]. + bacterial_c_n_ratio = 5.2 + """Ratio of carbon to nitrogen in bacterial 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. + on previous literature. """ - microbial_c_p_ratio = 16 - """Ratio of carbon to phosphorus in microbial biomass [unitless]. + bacterial_c_p_ratio = 16 + """Ratio of carbon to phosphorus in bacterial 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. + on previous literature. """ ammonium_mineralisation_proportion = 0.9 @@ -371,8 +369,8 @@ class SoilConsts(ConstantsDataclass): particularly clear. """ - max_uptake_rate_ammonium = 5e-3 - """Maximum possible rate for ammonium uptake [day^-1]. + max_bacterial_uptake_rate_ammonium = 5e-3 + """Maximum possible rate for bacterial ammonium uptake [day^-1]. This rate corresponds to the reference temperature given by :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by @@ -382,7 +380,7 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - half_sat_ammonium_uptake: float = 0.02275 + half_sat_bacterial_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 @@ -393,8 +391,8 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - max_uptake_rate_nitrate = 5e-4 - """Maximum possible rate for nitrate uptake [day^-1]. + max_bacterial_uptake_rate_nitrate = 5e-4 + """Maximum possible rate for bacterial nitrate uptake [day^-1]. This rate corresponds to the reference temperature given by :attr:`arrhenius_reference_temp`, with the corresponding activation energy given by @@ -404,8 +402,8 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - half_sat_nitrate_uptake: float = 0.02275 - """Half saturation constant for uptake of nitrate [kg N m^-3]. + half_sat_bacterial_nitrate_uptake: float = 0.02275 + """Half saturation constant for bacterial 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 @@ -415,7 +413,7 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - max_uptake_rate_labile_p = 0.0025 + max_bacterial_uptake_rate_labile_p = 0.0025 """Maximum possible rate for labile inorganic phosphorus uptake [day^-1]. This rate corresponds to the reference temperature given by @@ -426,7 +424,7 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - half_sat_labile_p_uptake: float = 0.02275 + half_sat_bacterial_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`, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 511df1872..03bbec7b0 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -67,8 +67,10 @@ 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].""" + + # TODO - Need to add a fungal change in here as well pom_enzyme_change: NDArray[np.float32] """Rate of change of particulate organic matter degrading enzyme pool. @@ -363,7 +365,7 @@ 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_bacteria, + soil_c_pool_bacteria=self.pools.soil_c_pool_bacteria, soil_enzyme_pom=self.pools.soil_enzyme_pom, soil_enzyme_maom=self.pools.soil_enzyme_maom, soil_temp=soil_temperature, @@ -546,9 +548,9 @@ def calculate_all_pool_updates( - enzyme_mediated.maom_to_lmwc - maom_desorption_to_lmwc ) - # TODO - Need to not just feed the same change to both - delta_pools_ordered["soil_c_pool_bacteria"] = microbial_changes.microbe_change - delta_pools_ordered["soil_c_pool_fungi"] = microbial_changes.microbe_change + delta_pools_ordered["soil_c_pool_bacteria"] = microbial_changes.bacteria_change + # TODO - This needs to become fungi_change once that's implemented + delta_pools_ordered["soil_c_pool_fungi"] = microbial_changes.bacteria_change delta_pools_ordered["soil_c_pool_pom"] = ( litter_mineralisation_flux.pom - enzyme_mediated.pom_to_lmwc ) @@ -640,7 +642,7 @@ 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_enzyme_pom: NDArray[np.float32], soil_enzyme_maom: NDArray[np.float32], soil_temp: NDArray[np.float32], @@ -661,7 +663,7 @@ 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_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 @@ -684,16 +686,18 @@ def calculate_microbial_changes( 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, + soil_c_pool_bacteria=soil_c_pool_bacteria, 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, + microbe_pool_size=soil_c_pool_bacteria, soil_temp=soil_temp, - constants=constants, + microbial_turnover_rate=constants.bacterial_turnover_rate, + activation_energy_turnover=constants.activation_energy_microbial_turnover, + reference_temperature=constants.arrhenius_reference_temp, ) # Find changes in each enzyme pool pom_enzyme_net_change, maom_enzyme_net_change, enzyme_denaturation = ( @@ -717,7 +721,7 @@ def calculate_microbial_changes( nitrate_change=microbial_uptake.nitrate, dop_uptake=microbial_uptake.organic_phosphorus, labile_p_change=microbial_uptake.inorganic_phosphorus, - microbe_change=biomass_growth - biomass_loss, + bacteria_change=biomass_growth - biomass_loss, pom_enzyme_change=pom_enzyme_net_change, maom_enzyme_change=maom_enzyme_net_change, necromass_generation=enzyme_denaturation + true_loss, @@ -904,20 +908,26 @@ 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_turnover_rate: float, + activation_energy_turnover: float, + 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_turnover_rate: microbial biomass turnover rate at reference + temperature [day^-1] + activation_energy_turnover: Activation energy for microbial maintenance turnover + rate [J K^-1] + 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 @@ -926,11 +936,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=activation_energy_turnover, + reference_temperature=reference_temperature, ) - return constants.microbial_turnover_rate * temp_factor * soil_c_pool_microbe + return microbial_turnover_rate * temp_factor * microbe_pool_size def calculate_carbon_use_efficiency( @@ -973,6 +983,7 @@ def calculate_enzyme_turnover( return turnover_rate * enzyme_pool +# TODO - This should also calculate fungal uptake rates def calculate_nutrient_uptake_rates( soil_c_pool_lmwc: NDArray[np.float32], soil_n_pool_don: NDArray[np.float32], @@ -980,7 +991,7 @@ 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], + soil_c_pool_bacteria: NDArray[np.float32], water_factor: NDArray[np.float32], pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], @@ -1018,7 +1029,7 @@ 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] + soil_c_pool_bacteria: Bacterial biomass (carbon) pool [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 @@ -1036,42 +1047,42 @@ 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, + soil_c_pool_microbe=soil_c_pool_bacteria, 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, + max_uptake_rate=constants.max_bacterial_uptake_rate_labile_C, + half_saturation_constant=constants.half_sat_bacterial_labile_C_uptake, constants=constants, ) ammonium_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_n_pool_ammonium, - soil_c_pool_microbe=soil_c_pool_microbe, + soil_c_pool_microbe=soil_c_pool_bacteria, 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, + max_uptake_rate=constants.max_bacterial_uptake_rate_ammonium, + half_saturation_constant=constants.half_sat_bacterial_ammonium_uptake, constants=constants, ) nitrate_uptake_rate_max = calculate_highest_achievable_nutrient_uptake( labile_nutrient_pool=soil_n_pool_nitrate, - soil_c_pool_microbe=soil_c_pool_microbe, + soil_c_pool_microbe=soil_c_pool_bacteria, 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, + max_uptake_rate=constants.max_bacterial_uptake_rate_nitrate, + half_saturation_constant=constants.half_sat_bacterial_nitrate_uptake, constants=constants, ) 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, + soil_c_pool_microbe=soil_c_pool_bacteria, 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, + max_uptake_rate=constants.max_bacterial_uptake_rate_labile_p, + half_saturation_constant=constants.half_sat_bacterial_labile_p_uptake, constants=constants, ) @@ -1097,13 +1108,13 @@ def calculate_nutrient_uptake_rates( actual_carbon_gain = np.minimum.reduce( [ carbon_gain_max, - constants.microbial_c_n_ratio + constants.bacterial_c_n_ratio * ( organic_nitrogen_uptake_rate_max + ammonium_uptake_rate_max + nitrate_uptake_rate_max ), - constants.microbial_c_p_ratio + constants.bacterial_c_p_ratio * ( organic_phosphorus_uptake_rate_max + inorganic_phosphorus_uptake_rate_max @@ -1118,7 +1129,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 / constants.bacterial_c_n_ratio inorganic_nitrogen_change = nitrogen_demand - actual_organic_nitrogen_uptake # For immobilisation of nitrogen, the proportion of ammonium and nitrate taken up # follows the proportion of the maximum uptake rates, for the mineralisation it is @@ -1133,7 +1144,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 / constants.bacterial_c_p_ratio inorganic_phosphorus_change = phosphorus_demand - actual_organic_phosphorus_uptake consumption_rates = NetNutrientConsumption( @@ -1166,12 +1177,14 @@ def calculate_highest_achievable_nutrient_uptake( 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] + soil_c_pool_microbe: 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 @@ -1488,9 +1501,10 @@ def calculate_nutrient_flows_to_necromass( [kg P m^-3 day^-1] are added to the soil necromass pool """ + # TODO - Fungi need to be added here return ( - microbial_changes.necromass_generation / constants.microbial_c_n_ratio, - microbial_changes.necromass_generation / constants.microbial_c_p_ratio, + microbial_changes.necromass_generation / constants.bacterial_c_n_ratio, + microbial_changes.necromass_generation / constants.bacterial_c_p_ratio, ) From 84c9b5123af73f659c90653bdcfe3dd03b44e98b Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Tue, 25 Feb 2025 15:57:25 +0000 Subject: [PATCH 3/8] Refactored the soil model code so that fungi and bacteria both take up nutrients, produce enzymes, grow, etc --- tests/models/soil/conftest.py | 13 + tests/models/soil/test_functional_groups.py | 54 ++++ tests/models/soil/test_pools.py | 96 ++++--- tests/models/soil/test_soil_model.py | 139 +++++----- virtual_ecosystem/models/soil/constants.py | 162 ++++++++++-- .../models/soil/functional_groups.py | 136 ++++++++++ virtual_ecosystem/models/soil/pools.py | 240 ++++++++++++------ virtual_ecosystem/models/soil/soil_model.py | 8 + 8 files changed, 651 insertions(+), 197 deletions(-) create mode 100644 tests/models/soil/test_functional_groups.py create mode 100644 virtual_ecosystem/models/soil/functional_groups.py diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index d8960ce8f..ed50bd07e 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -177,3 +177,16 @@ 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( + dummy_carbon_data, fixture_core_components, environmental_factors +): + """Set of functional groups based on the soil model constants.""" + from virtual_ecosystem.models.soil.constants import SoilConsts + from virtual_ecosystem.models.soil.functional_groups import ( + make_full_set_of_functional_groups, + ) + + return make_full_set_of_functional_groups(constants=SoilConsts) diff --git a/tests/models/soil/test_functional_groups.py b/tests/models/soil/test_functional_groups.py new file mode 100644 index 000000000..8ee856d05 --- /dev/null +++ b/tests/models/soil/test_functional_groups.py @@ -0,0 +1,54 @@ +"""Test module for soil.functional_groups.py. + +This module tests the functions which generate functional groups. +""" + + +def test_make_full_set_of_functional_groups(): + """Test that the function to make all the functional group works.""" + from virtual_ecosystem.models.soil.constants import SoilConsts + from virtual_ecosystem.models.soil.functional_groups import ( + FunctionalGroup, + make_full_set_of_functional_groups, + ) + + expected_groups = ["bacteria", "fungi"] + + functional_groups = make_full_set_of_functional_groups(SoilConsts) + + assert set(expected_groups) == set(functional_groups.keys()) + + for group in expected_groups: + assert type(functional_groups[group]) is FunctionalGroup + + # 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 + + +def test_make_bacterial_functional_group(): + """Test that the function to make the bacterial functional group works.""" + from virtual_ecosystem.models.soil.constants import SoilConsts + from virtual_ecosystem.models.soil.functional_groups import ( + FunctionalGroup, + make_bacterial_functional_group, + ) + + bacterial_group = make_bacterial_functional_group(SoilConsts) + assert type(bacterial_group) is FunctionalGroup + # Only testing one value, as testing them all seems like overkill/hard to maintain + assert bacterial_group.c_n_ratio == 5.2 + + +def test_make_fungal_functional_group(): + """Test that the function to make the fungal functional group works.""" + from virtual_ecosystem.models.soil.constants import SoilConsts + from virtual_ecosystem.models.soil.functional_groups import ( + FunctionalGroup, + make_fungal_functional_group, + ) + + fungal_group = make_fungal_functional_group(SoilConsts) + assert type(fungal_group) is FunctionalGroup + # Only testing one value, as testing them all seems like overkill/hard to maintain + assert fungal_group.c_n_ratio == 6.5 diff --git a/tests/models/soil/test_pools.py b/tests/models/soil/test_pools.py index 3220b9339..41ead622a 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,31 +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.014984117633, 0.0133384581, 0.03449812333, 0.02425546], + "soil_c_pool_lmwc": [0.014909863, 0.0026977357, 0.03275147271, 0.023993336945], "soil_c_pool_maom": [0.038767651, 0.00829848, 0.05982197, 0.07277182], "soil_c_pool_bacteria": [-0.054361097, -0.022606231, -0.118911406, -0.00719517], - "soil_c_pool_fungi": [-0.054361097, -0.022606231, -0.118911406, -0.007195167], + "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.000952008, 0.019913667, 0.000505414, 0.000455603], - "soil_n_pool_nitrate": [-0.000293386, -1.292735e-5, -3.576543e-5, -0.000255954], - "soil_p_pool_dop": [0.000194453, 7.1014337e-5, 0.0001851685, 0.0001017010], + "soil_n_pool_ammonium": [0.00095125671, 0.02011151359, 0.00043745, 0.000572988], + "soil_n_pool_nitrate": [-0.000295899, 9.0556049e-6, -4.592098e-5, -0.000242911], + "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": [-3.772779e-6, -1.947773e-5, -7.260241e-5, -1.591909e-7], + "soil_p_pool_labile": [-4.432585e-6, -9.510288e-5, -8.470421e-5, 2.6867148e-6], } # Make order of pools object @@ -86,23 +89,26 @@ def test_calculate_all_pool_updates(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], + "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], - "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], + "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( @@ -113,6 +119,7 @@ def test_calculate_microbial_changes( 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_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"][ @@ -120,6 +127,7 @@ def test_calculate_microbial_changes( ], env_factors=environmental_factors, constants=SoilConsts, + functional_groups=functional_groups, ) for attr in dir(actual_mic_changes): @@ -198,16 +206,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, ) @@ -286,7 +298,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 ( @@ -310,13 +322,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_bacteria=dummy_carbon_data["soil_c_pool_bacteria"], + 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) @@ -344,15 +357,17 @@ 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_bacteria"], + 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_bacterial_uptake_rate_labile_C, + activation_energy_uptake=SoilConsts.activation_energy_microbial_uptake, half_saturation_constant=SoilConsts.half_sat_bacterial_labile_C_uptake, - constants=SoilConsts, + activation_energy_uptake_saturation=SoilConsts.activation_energy_uptake_saturation, + reference_temperature=SoilConsts.arrhenius_reference_temp, ) assert np.allclose(actual_uptake, expected_uptake) @@ -519,18 +534,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(): """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, + constants=SoilConsts, ) ) diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index 066bf7321..550463568 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -300,63 +300,63 @@ def test_update(mocker, fixture_soil_model, dummy_carbon_data): Dataset( data_vars=dict( soil_c_pool_lmwc=DataArray( - [0.05713292, 0.02665739, 0.11689175, 0.01486035], dims="cell_id" + [0.05714338, 0.02206952, 0.11592186, 0.01538873], 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_bacteria=DataArray( - [5.77303027, 2.2888041, 11.24109943, 0.9964216], + [5.77302395, 2.28877945, 11.24105325, 0.99642196], dims="cell_id", ), soil_c_pool_fungi=DataArray( - [0.86302169, 8.53880051, 2.15105403, 4.5364215], + [0.88590099, 8.509643, 2.19875113, 4.52376326], 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.06167057, 0.05209252, 0.11550511, 0.08189107], 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.00135906, 0.00340964, 0.00273513, 0.00390386], dims="cell_id" + [0.00138664, 0.00297325, 0.00279915, 0.00393828], 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.00053642, 0.01499882, 0.00044842, 0.00538707], + [0.00053212, 0.01537305, 0.00040683, 0.00544821], dims="cell_id", ), soil_n_pool_nitrate=DataArray( - [0.00189682, 0.0038413, 0.00031329, 0.01290568], dims="cell_id" + [0.00189446, 0.00388282, 0.00030788, 0.01291297], dims="cell_id" ), soil_p_pool_dop=DataArray( - [1.68559250e-4, 9.03050817e-5, 3.15038568e-4, 1.66029558e-4], + [0.00017326, 0.00012043, 0.00032658, 0.00018222], 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" @@ -365,7 +365,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( - [9.00903822e-7, 1.92559822e-5, 1.37908411e-5, 1.93794964e-4], + [-5.39902139e-7, -1.35782323e-5, 4.95128072e-6, 1.94311546e-4], dims="cell_id", ), ) @@ -479,7 +479,9 @@ def test_order_independance( assert np.allclose(output[pool_name], output_reversed[pool_name]) -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 @@ -489,10 +491,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): ) delta_pools = [ - 0.014984117633, - 0.0133384581, - 0.03449812333, - 0.02425546, + 0.014909863, + 0.0026977357, + 0.03275147271, + 0.023993336945, 0.038767651, 0.00829848, 0.05982197, @@ -501,62 +503,62 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -0.022606231, -0.118911406, -0.007195167, - -0.054361097, - -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.000952008, - 0.019913667, - 0.000505414, - 0.000455603, - -0.000293386, - -1.292735e-5, - -3.576543e-5, - -0.000255954, - 0.000194453, - 7.1014337e-5, - 0.0001851685, - 0.0001017010, + 0.00095125671, + 0.02011151359, + 0.00043745, + 0.000572988, + -0.000295899, + 9.0556049e-6, + -4.592098e-5, + -0.000242911, + 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, @@ -569,10 +571,10 @@ def test_construct_full_soil_model(dummy_carbon_data, fixture_core_components): -2.77311e-6, -7.40324e-7, -2.187697e-7, - -3.772779e-6, - -1.947773e-5, - -7.260241e-5, - -1.591909e-7, + -4.432585e-6, + -9.510288e-5, + -8.470421e-5, + 2.6867148e-6, ] # make pools @@ -599,6 +601,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/virtual_ecosystem/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index ae70f05e6..195e3a156 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -57,7 +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_bacterial_uptake_rate_labile_C: float = 0.04 """Maximum rate at the reference temperature of labile carbon uptake [day^-1]. @@ -69,6 +68,17 @@ class SoilConsts(ConstantsDataclass): once fungi are added. """ + max_fungal_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 bacterial nutrient uptake [J K^-1]. @@ -88,6 +98,16 @@ class SoilConsts(ConstantsDataclass): :attr:`activation_energy_uptake_saturation`. """ + half_sat_fungal_labile_C_uptake: float = 0.364 + """Half saturation constant for fungal 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]. @@ -157,7 +177,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 bacterial_turnover_rate: float = 0.005 """Bacterial turnover rate at reference temperature [day^-1]. @@ -176,6 +195,17 @@ class SoilConsts(ConstantsDataclass): this activation energy corresponds to is given by :attr:`bacterial_turnover_rate`. """ + fungal_turnover_rate: float = 0.005 + """Fungal 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. + """ + # 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,14 +220,26 @@ 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`. """ - maintenance_maom_enzyme: float = 1e-2 - """Fraction of maintenance synthesis used to produce MAOM degrading enzymes. + 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`. + """ + + 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`. """ @@ -310,28 +352,28 @@ class SoilConsts(ConstantsDataclass): environmental conditions is a post release goal. """ - litter_leaching_fraction_carbon = 0.0015 + litter_leaching_fraction_carbon: float = 0.0015 """Fraction of carbon 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`. """ - litter_leaching_fraction_nitrogen = 0.0015 + litter_leaching_fraction_nitrogen: float = 0.0015 """Fraction of nitrogen 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`. """ - litter_leaching_fraction_phosphorus = 0.0001 + litter_leaching_fraction_phosphorus: float = 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`. """ - organic_proportion_litter_nitrogen_leaching = 1.0 + organic_proportion_litter_nitrogen_leaching: float = 1.0 """Fraction of leached nitrogen from litter mineralisation that is organic form. [unitless]. The remainder of the leaching consists of ammonium. Value is taken from @@ -339,7 +381,7 @@ class SoilConsts(ConstantsDataclass): litter solely in organic form. """ - organic_proportion_litter_phosphorus_leaching = 1.0 + organic_proportion_litter_phosphorus_leaching: float = 1.0 """Fraction of leached phosphorus from litter mineralisation that is organic form. [unitless]. The remainder of the leaching consists of inorganic phosphorus. Value is @@ -347,21 +389,35 @@ class SoilConsts(ConstantsDataclass): leaches from litter solely in organic form. """ - bacterial_c_n_ratio = 5.2 + bacterial_c_n_ratio: float = 5.2 """Ratio of carbon to nitrogen in bacterial biomass [unitless]. Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based on previous literature. """ - bacterial_c_p_ratio = 16 + bacterial_c_p_ratio: float = 16 """Ratio of carbon to phosphorus in bacterial biomass [unitless]. Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based on previous literature. """ - ammonium_mineralisation_proportion = 0.9 + fungal_c_n_ratio: float = 6.5 + """Ratio of carbon to nitrogen in fungal biomass [unitless]. + + Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based + on previous literature. + """ + + fungal_c_p_ratio: float = 40.0 + """Ratio of carbon to phosphorus in fungal biomass [unitless]. + + Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based + on previous literature. + """ + + ammonium_mineralisation_proportion: float = 0.9 """Proportion of microbially mineralised nitrogen that takes the form of ammonium. [unitless]. The remainder gets mineralised as nitrate. Estimate taken from @@ -369,7 +425,7 @@ class SoilConsts(ConstantsDataclass): particularly clear. """ - max_bacterial_uptake_rate_ammonium = 5e-3 + max_bacterial_uptake_rate_ammonium: float = 5e-3 """Maximum possible rate for bacterial ammonium uptake [day^-1]. This rate corresponds to the reference temperature given by @@ -381,7 +437,7 @@ class SoilConsts(ConstantsDataclass): """ half_sat_bacterial_ammonium_uptake: float = 0.02275 - """Half saturation constant for uptake of ammonium [kg N m^-3]. + """Half saturation constant for bacterial 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 @@ -391,7 +447,7 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - max_bacterial_uptake_rate_nitrate = 5e-4 + max_bacterial_uptake_rate_nitrate: float = 5e-4 """Maximum possible rate for bacterial nitrate uptake [day^-1]. This rate corresponds to the reference temperature given by @@ -413,8 +469,8 @@ class SoilConsts(ConstantsDataclass): be better pinned down. """ - max_bacterial_uptake_rate_labile_p = 0.0025 - """Maximum possible rate for labile inorganic phosphorus uptake [day^-1]. + max_bacterial_uptake_rate_labile_p: float = 0.0025 + """Maximum possible rate for bacterial 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 @@ -425,7 +481,73 @@ class SoilConsts(ConstantsDataclass): """ half_sat_bacterial_labile_p_uptake: float = 0.02275 - """Half saturation constant for uptake of labile inorganic phosphorus. + """Half saturation constant for bacterial 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. + """ + + max_fungal_uptake_rate_ammonium = 5e-3 + """Maximum possible rate for fungal 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_fungal_ammonium_uptake: float = 0.02275 + """Half saturation constant for fungal 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_fungal_uptake_rate_nitrate = 5e-4 + """Maximum possible rate for fungal 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_fungal_nitrate_uptake: float = 0.02275 + """Half saturation constant for fungal 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_fungal_uptake_rate_labile_p: float = 0.0025 + """Maximum possible rate for fungal 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_fungal_labile_p_uptake: float = 0.02275 + """Half saturation constant for fungal 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 diff --git a/virtual_ecosystem/models/soil/functional_groups.py b/virtual_ecosystem/models/soil/functional_groups.py new file mode 100644 index 000000000..5106bcf44 --- /dev/null +++ b/virtual_ecosystem/models/soil/functional_groups.py @@ -0,0 +1,136 @@ +"""The ``models.soil.functional_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.models.soil.constants import SoilConsts + + +@dataclass +class FunctionalGroup: + """Base class for microbial functional groups. + + This sets out the constants which must be defined for each microbial functional + group. + """ + + 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_functional_groups( + constants: SoilConsts, +) -> dict[str, FunctionalGroup]: + """Make the full set of functional groups used in the soil model. + + Args: + constants: The constants for the soil model. + + Returns: + A dictionary containing each functional group used in the soil model (currently + bacteria and fungi). + """ + + return { + "bacteria": make_bacterial_functional_group(constants), + "fungi": make_fungal_functional_group(constants), + } + + +def make_bacterial_functional_group(constants: SoilConsts) -> FunctionalGroup: + """Collect the constants for the bacterial functional group. + + Args: + constants: The constants for the soil model. + + Returns: + A ``FunctionalGroup`` object parameterized with the full set of constants needed + to define the bacterial functional group. + """ + + return FunctionalGroup( + max_uptake_rate_labile_C=constants.max_bacterial_uptake_rate_labile_C, + activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, + half_sat_labile_C_uptake=constants.half_sat_bacterial_labile_C_uptake, + activation_energy_uptake_saturation=constants.activation_energy_uptake_saturation, + max_uptake_rate_ammonium=constants.max_bacterial_uptake_rate_ammonium, + half_sat_ammonium_uptake=constants.half_sat_bacterial_ammonium_uptake, + max_uptake_rate_nitrate=constants.max_bacterial_uptake_rate_nitrate, + half_sat_nitrate_uptake=constants.half_sat_bacterial_nitrate_uptake, + max_uptake_rate_labile_p=constants.max_bacterial_uptake_rate_labile_p, + half_sat_labile_p_uptake=constants.half_sat_bacterial_labile_p_uptake, + turnover_rate=constants.bacterial_turnover_rate, + activation_energy_turnover=constants.activation_energy_microbial_turnover, + c_n_ratio=constants.bacterial_c_n_ratio, + c_p_ratio=constants.bacterial_c_p_ratio, + ) + + +def make_fungal_functional_group(constants: SoilConsts) -> FunctionalGroup: + """Collect the constants for the fungal functional group. + + Args: + constants: The constants for the soil model. + + Returns: + A ``FunctionalGroup`` object parameterized with the full set of constants needed + to define the fungal functional group. + """ + + return FunctionalGroup( + max_uptake_rate_labile_C=constants.max_fungal_uptake_rate_labile_C, + activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, + half_sat_labile_C_uptake=constants.half_sat_fungal_labile_C_uptake, + activation_energy_uptake_saturation=constants.activation_energy_uptake_saturation, + max_uptake_rate_ammonium=constants.max_fungal_uptake_rate_ammonium, + half_sat_ammonium_uptake=constants.half_sat_fungal_ammonium_uptake, + max_uptake_rate_nitrate=constants.max_fungal_uptake_rate_nitrate, + half_sat_nitrate_uptake=constants.half_sat_fungal_nitrate_uptake, + max_uptake_rate_labile_p=constants.max_fungal_uptake_rate_labile_p, + half_sat_labile_p_uptake=constants.half_sat_fungal_labile_p_uptake, + turnover_rate=constants.fungal_turnover_rate, + activation_energy_turnover=constants.activation_energy_microbial_turnover, + c_n_ratio=constants.fungal_c_n_ratio, + c_p_ratio=constants.fungal_c_p_ratio, + ) diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 03bbec7b0..463702f34 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.functional_groups import FunctionalGroup # TODO - At this point in time I'm not adding specific phosphatase enzymes, need to # think about adding these in future @@ -70,7 +71,8 @@ class MicrobialChanges: bacteria_change: NDArray[np.float32] """Rate of change of bacterial biomass pool [kg C m^-3 day^-1].""" - # TODO - Need to add a fungal change in here as well + 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. @@ -87,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: @@ -281,6 +289,7 @@ def __init__( data: Data, pools: dict[str, NDArray[np.float32]], constants: SoilConsts, + functional_groups: dict[str, FunctionalGroup], max_depth_of_microbial_activity: float, ): self.data = data @@ -295,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].""" @@ -356,7 +368,6 @@ def calculate_all_pool_updates( clay_fraction=self.data["clay_fraction"].to_numpy(), constants=self.constants, ) - # TODO - This needs to be changed to accept both fungi and bacteria # find changes related to microbial uptake, growth and decay microbial_changes = calculate_microbial_changes( soil_c_pool_lmwc=self.pools.soil_c_pool_lmwc, @@ -366,11 +377,13 @@ def calculate_all_pool_updates( soil_p_pool_dop=self.pools.soil_p_pool_dop, soil_p_pool_labile=self.pools.soil_p_pool_labile, 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( @@ -442,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, @@ -549,8 +558,7 @@ def calculate_all_pool_updates( - maom_desorption_to_lmwc ) delta_pools_ordered["soil_c_pool_bacteria"] = microbial_changes.bacteria_change - # TODO - This needs to become fungi_change once that's implemented - delta_pools_ordered["soil_c_pool_fungi"] = 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 ) @@ -573,7 +581,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"] ) @@ -609,7 +617,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"] ) @@ -643,11 +651,13 @@ def calculate_microbial_changes( soil_p_pool_dop: NDArray[np.float32], soil_p_pool_labile: 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, FunctionalGroup], ) -> MicrobialChanges: """Calculate the changes for the microbial biomass and enzyme pools. @@ -664,6 +674,7 @@ def calculate_microbial_changes( 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_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 @@ -672,6 +683,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 @@ -679,52 +691,99 @@ 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_bacteria=soil_c_pool_bacteria, + 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( + bacterial_biomass_loss = calculate_maintenance_biomass_synthesis( microbe_pool_size=soil_c_pool_bacteria, soil_temp=soil_temp, microbial_turnover_rate=constants.bacterial_turnover_rate, activation_energy_turnover=constants.activation_energy_microbial_turnover, 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_turnover_rate=constants.fungal_turnover_rate, + activation_energy_turnover=constants.activation_energy_microbial_turnover, + 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, + constants=constants, + ) 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, - bacteria_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, ) @@ -863,7 +922,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. @@ -877,9 +937,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: @@ -888,8 +951,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, @@ -983,7 +1052,6 @@ def calculate_enzyme_turnover( return turnover_rate * enzyme_pool -# TODO - This should also calculate fungal uptake rates def calculate_nutrient_uptake_rates( soil_c_pool_lmwc: NDArray[np.float32], soil_n_pool_don: NDArray[np.float32], @@ -991,11 +1059,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_bacteria: 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: FunctionalGroup, ) -> tuple[NDArray[np.float32], NetNutrientConsumption]: """Calculate the rate at which microbes uptake each nutrient. @@ -1029,13 +1098,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_bacteria: Bacterial 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 @@ -1047,43 +1118,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_bacteria, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_bacterial_uptake_rate_labile_C, - half_saturation_constant=constants.half_sat_bacterial_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_bacteria, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_bacterial_uptake_rate_ammonium, - half_saturation_constant=constants.half_sat_bacterial_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_bacteria, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_bacterial_uptake_rate_nitrate, - half_saturation_constant=constants.half_sat_bacterial_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_bacteria, + microbial_pool_size=microbial_pool_size, water_factor=water_factor, pH_factor=pH_factor, soil_temp=soil_temp, - max_uptake_rate=constants.max_bacterial_uptake_rate_labile_p, - half_saturation_constant=constants.half_sat_bacterial_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 @@ -1108,13 +1187,13 @@ def calculate_nutrient_uptake_rates( actual_carbon_gain = np.minimum.reduce( [ carbon_gain_max, - constants.bacterial_c_n_ratio + functional_group.c_n_ratio * ( organic_nitrogen_uptake_rate_max + ammonium_uptake_rate_max + nitrate_uptake_rate_max ), - constants.bacterial_c_p_ratio + functional_group.c_p_ratio * ( organic_phosphorus_uptake_rate_max + inorganic_phosphorus_uptake_rate_max @@ -1129,7 +1208,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.bacterial_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 # follows the proportion of the maximum uptake rates, for the mineralisation it is @@ -1144,7 +1223,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.bacterial_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( @@ -1165,13 +1244,15 @@ 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. @@ -1183,7 +1264,7 @@ def calculate_highest_achievable_nutrient_uptake( Args: labile_nutrient_pool: Mass of nutrient that is in a readily uptakeable (labile) form [kg nut m^-3] - soil_c_pool_microbe: Size of microbial biomass (carbon) pool of interest [kg C + 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] @@ -1192,9 +1273,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 @@ -1204,13 +1289,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 @@ -1220,7 +1305,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) ) @@ -1479,21 +1564,21 @@ 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], + constants: SoilConsts, ) -> 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 + 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] constants: Set of constants for the soil model. Returns: @@ -1501,10 +1586,17 @@ def calculate_nutrient_flows_to_necromass( [kg P m^-3 day^-1] are added to the soil necromass pool """ - # TODO - Fungi need to be added here + # 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.bacterial_c_n_ratio, - microbial_changes.necromass_generation / constants.bacterial_c_p_ratio, + (bacterial_loss / constants.bacterial_c_n_ratio) + + (fungal_loss / constants.fungal_c_n_ratio) + + (enzyme_denaturation / constants.bacterial_c_n_ratio), + (bacterial_loss / constants.bacterial_c_p_ratio) + + (fungal_loss / constants.fungal_c_p_ratio) + + (enzyme_denaturation / constants.bacterial_c_p_ratio), ) diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index fded7af08..db6fd1249 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.functional_groups import ( + FunctionalGroup, + make_full_set_of_functional_groups, +) from virtual_ecosystem.models.soil.pools import SoilPools @@ -291,6 +295,7 @@ def integrate(self) -> dict[str, DataArray]: self.layer_structure.index_topsoil_scalar, delta_pools_ordered, self.model_constants, + make_full_set_of_functional_groups(self.model_constants), self.core_constants.max_depth_of_microbial_activity, self.core_constants.soil_moisture_capacity, self.layer_structure.soil_layer_thickness[0], @@ -326,6 +331,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, FunctionalGroup], max_depth_of_microbial_activity: float, soil_moisture_capacity: float, top_soil_layer_thickness: float, @@ -343,6 +349,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 @@ -365,6 +372,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, ) From e37bd81e06d6a778d3ab600968121d2a72d341e6 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 26 Feb 2025 08:08:52 +0000 Subject: [PATCH 4/8] Added functional groups into the api docs --- docs/source/_toc.yaml | 2 ++ .../api/models/soil/functional_groups.md | 33 +++++++++++++++++++ virtual_ecosystem/models/soil/__init__.py | 6 ++-- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 docs/source/api/models/soil/functional_groups.md diff --git a/docs/source/_toc.yaml b/docs/source/_toc.yaml index 5b6042ae2..6b6306a28 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/functional_groups + title: The functional_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/functional_groups.md b/docs/source/api/models/soil/functional_groups.md new file mode 100644 index 000000000..520bfb05f --- /dev/null +++ b/docs/source/api/models/soil/functional_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.functional_groups` module + +```{eval-rst} +.. automodule:: virtual_ecosystem.models.soil.functional_groups + :autosummary: + :members: +``` diff --git a/virtual_ecosystem/models/soil/__init__.py b/virtual_ecosystem/models/soil/__init__.py index 732439124..4d217b962 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.functional_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 From bb92717e2a26c94d31a0078f86abc8d4fc6ccfa9 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Wed, 26 Feb 2025 08:27:30 +0000 Subject: [PATCH 5/8] Renamed FunctionalGroup to MicrobialGroup to avoid a naming conflict with the animal module --- docs/source/_toc.yaml | 4 +-- ...nctional_groups.md => microbial_groups.md} | 4 +-- tests/models/soil/conftest.py | 6 ++-- ...nal_groups.py => test_microbial_groups.py} | 30 +++++++++---------- virtual_ecosystem/models/soil/__init__.py | 2 +- ...nctional_groups.py => microbial_groups.py} | 26 ++++++++-------- virtual_ecosystem/models/soil/pools.py | 8 ++--- virtual_ecosystem/models/soil/soil_model.py | 10 +++---- 8 files changed, 46 insertions(+), 44 deletions(-) rename docs/source/api/models/soil/{functional_groups.md => microbial_groups.md} (87%) rename tests/models/soil/{test_functional_groups.py => test_microbial_groups.py} (62%) rename virtual_ecosystem/models/soil/{functional_groups.py => microbial_groups.py} (85%) diff --git a/docs/source/_toc.yaml b/docs/source/_toc.yaml index 6b6306a28..d8bf36bca 100644 --- a/docs/source/_toc.yaml +++ b/docs/source/_toc.yaml @@ -202,8 +202,8 @@ subtrees: title: The constants submodule - file: api/models/soil/env_factors title: The env_factors submodule - - file: api/models/soil/functional_groups - title: The functional_groups 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/functional_groups.md b/docs/source/api/models/soil/microbial_groups.md similarity index 87% rename from docs/source/api/models/soil/functional_groups.md rename to docs/source/api/models/soil/microbial_groups.md index 520bfb05f..855891c8f 100644 --- a/docs/source/api/models/soil/functional_groups.md +++ b/docs/source/api/models/soil/microbial_groups.md @@ -24,10 +24,10 @@ language_info: version: 3.11.9 --- -# API documentation for the {mod}`~virtual_ecosystem.models.soil.functional_groups` module +# API documentation for the {mod}`~virtual_ecosystem.models.soil.microbial_groups` module ```{eval-rst} -.. automodule:: virtual_ecosystem.models.soil.functional_groups +.. automodule:: virtual_ecosystem.models.soil.microbial_groups :autosummary: :members: ``` diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index ed50bd07e..61663dea8 100644 --- a/tests/models/soil/conftest.py +++ b/tests/models/soil/conftest.py @@ -185,8 +185,8 @@ def functional_groups( ): """Set of functional groups based on the soil model constants.""" from virtual_ecosystem.models.soil.constants import SoilConsts - from virtual_ecosystem.models.soil.functional_groups import ( - make_full_set_of_functional_groups, + from virtual_ecosystem.models.soil.microbial_groups import ( + make_full_set_of_microbial_groups, ) - return make_full_set_of_functional_groups(constants=SoilConsts) + return make_full_set_of_microbial_groups(constants=SoilConsts) diff --git a/tests/models/soil/test_functional_groups.py b/tests/models/soil/test_microbial_groups.py similarity index 62% rename from tests/models/soil/test_functional_groups.py rename to tests/models/soil/test_microbial_groups.py index 8ee856d05..88809f8e0 100644 --- a/tests/models/soil/test_functional_groups.py +++ b/tests/models/soil/test_microbial_groups.py @@ -1,25 +1,25 @@ -"""Test module for soil.functional_groups.py. +"""Test module for soil.microbial_groups.py. -This module tests the functions which generate functional groups. +This module tests the functions which generate microbial functional groups. """ -def test_make_full_set_of_functional_groups(): - """Test that the function to make all the functional group works.""" +def test_make_full_set_of_microbial_groups(): + """Test that the function to make all the microbial group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts - from virtual_ecosystem.models.soil.functional_groups import ( - FunctionalGroup, - make_full_set_of_functional_groups, + from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroup, + make_full_set_of_microbial_groups, ) expected_groups = ["bacteria", "fungi"] - functional_groups = make_full_set_of_functional_groups(SoilConsts) + functional_groups = make_full_set_of_microbial_groups(SoilConsts) assert set(expected_groups) == set(functional_groups.keys()) for group in expected_groups: - assert type(functional_groups[group]) is FunctionalGroup + assert type(functional_groups[group]) is MicrobialGroup # Only testing one value, as testing them all seems like overkill/hard to maintain assert functional_groups["bacteria"].c_n_ratio == 5.2 @@ -29,13 +29,13 @@ def test_make_full_set_of_functional_groups(): def test_make_bacterial_functional_group(): """Test that the function to make the bacterial functional group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts - from virtual_ecosystem.models.soil.functional_groups import ( - FunctionalGroup, + from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroup, make_bacterial_functional_group, ) bacterial_group = make_bacterial_functional_group(SoilConsts) - assert type(bacterial_group) is FunctionalGroup + assert type(bacterial_group) is MicrobialGroup # Only testing one value, as testing them all seems like overkill/hard to maintain assert bacterial_group.c_n_ratio == 5.2 @@ -43,12 +43,12 @@ def test_make_bacterial_functional_group(): def test_make_fungal_functional_group(): """Test that the function to make the fungal functional group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts - from virtual_ecosystem.models.soil.functional_groups import ( - FunctionalGroup, + from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroup, make_fungal_functional_group, ) fungal_group = make_fungal_functional_group(SoilConsts) - assert type(fungal_group) is FunctionalGroup + assert type(fungal_group) is MicrobialGroup # Only testing one value, as testing them all seems like overkill/hard to maintain assert fungal_group.c_n_ratio == 6.5 diff --git a/virtual_ecosystem/models/soil/__init__.py b/virtual_ecosystem/models/soil/__init__.py index 4d217b962..b1e06f82b 100644 --- a/virtual_ecosystem/models/soil/__init__.py +++ b/virtual_ecosystem/models/soil/__init__.py @@ -10,7 +10,7 @@ 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.functional_groups` provides the microbial +* 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. diff --git a/virtual_ecosystem/models/soil/functional_groups.py b/virtual_ecosystem/models/soil/microbial_groups.py similarity index 85% rename from virtual_ecosystem/models/soil/functional_groups.py rename to virtual_ecosystem/models/soil/microbial_groups.py index 5106bcf44..9c8ab016c 100644 --- a/virtual_ecosystem/models/soil/functional_groups.py +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -1,4 +1,4 @@ -"""The ``models.soil.functional_groups`` module contains the classes needed to define +"""The ``models.soil.microbial_groups`` module contains the classes needed to define the different microbial functional groups used in the soil model. """ # noqa: D205 @@ -8,7 +8,7 @@ @dataclass -class FunctionalGroup: +class MicrobialGroup: """Base class for microbial functional groups. This sets out the constants which must be defined for each microbial functional @@ -59,9 +59,9 @@ class FunctionalGroup: """Ratio of carbon to phosphorus in biomass [unitless].""" -def make_full_set_of_functional_groups( +def make_full_set_of_microbial_groups( constants: SoilConsts, -) -> dict[str, FunctionalGroup]: +) -> dict[str, MicrobialGroup]: """Make the full set of functional groups used in the soil model. Args: @@ -78,18 +78,19 @@ def make_full_set_of_functional_groups( } -def make_bacterial_functional_group(constants: SoilConsts) -> FunctionalGroup: +def make_bacterial_functional_group(constants: SoilConsts) -> MicrobialGroup: """Collect the constants for the bacterial functional group. Args: constants: The constants for the soil model. Returns: - A ``FunctionalGroup`` object parameterized with the full set of constants needed - to define the bacterial functional group. + A :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroup` object + parameterized with the full set of constants needed to define the bacterial + functional group. """ - return FunctionalGroup( + return MicrobialGroup( max_uptake_rate_labile_C=constants.max_bacterial_uptake_rate_labile_C, activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, half_sat_labile_C_uptake=constants.half_sat_bacterial_labile_C_uptake, @@ -107,18 +108,19 @@ def make_bacterial_functional_group(constants: SoilConsts) -> FunctionalGroup: ) -def make_fungal_functional_group(constants: SoilConsts) -> FunctionalGroup: +def make_fungal_functional_group(constants: SoilConsts) -> MicrobialGroup: """Collect the constants for the fungal functional group. Args: constants: The constants for the soil model. Returns: - A ``FunctionalGroup`` object parameterized with the full set of constants needed - to define the fungal functional group. + A :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroup` object + parameterized with the full set of constants needed to define the fungal + functional group. """ - return FunctionalGroup( + return MicrobialGroup( max_uptake_rate_labile_C=constants.max_fungal_uptake_rate_labile_C, activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, half_sat_labile_C_uptake=constants.half_sat_fungal_labile_C_uptake, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 463702f34..cdf83b61c 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -24,7 +24,7 @@ calculate_symbiotic_nitrogen_fixation_carbon_cost, calculate_temperature_effect_on_microbes, ) -from virtual_ecosystem.models.soil.functional_groups import FunctionalGroup +from virtual_ecosystem.models.soil.microbial_groups import MicrobialGroup # TODO - At this point in time I'm not adding specific phosphatase enzymes, need to # think about adding these in future @@ -289,7 +289,7 @@ def __init__( data: Data, pools: dict[str, NDArray[np.float32]], constants: SoilConsts, - functional_groups: dict[str, FunctionalGroup], + functional_groups: dict[str, MicrobialGroup], max_depth_of_microbial_activity: float, ): self.data = data @@ -657,7 +657,7 @@ def calculate_microbial_changes( soil_temp: NDArray[np.float32], env_factors: EnvironmentalEffectFactors, constants: SoilConsts, - functional_groups: dict[str, FunctionalGroup], + functional_groups: dict[str, MicrobialGroup], ) -> MicrobialChanges: """Calculate the changes for the microbial biomass and enzyme pools. @@ -1064,7 +1064,7 @@ def calculate_nutrient_uptake_rates( pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], constants: SoilConsts, - functional_group: FunctionalGroup, + functional_group: MicrobialGroup, ) -> tuple[NDArray[np.float32], NetNutrientConsumption]: """Calculate the rate at which microbes uptake each nutrient. diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index db6fd1249..877daca4d 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -32,9 +32,9 @@ 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.functional_groups import ( - FunctionalGroup, - make_full_set_of_functional_groups, +from virtual_ecosystem.models.soil.microbial_groups import ( + MicrobialGroup, + make_full_set_of_microbial_groups, ) from virtual_ecosystem.models.soil.pools import SoilPools @@ -295,7 +295,7 @@ def integrate(self) -> dict[str, DataArray]: self.layer_structure.index_topsoil_scalar, delta_pools_ordered, self.model_constants, - make_full_set_of_functional_groups(self.model_constants), + make_full_set_of_microbial_groups(self.model_constants), self.core_constants.max_depth_of_microbial_activity, self.core_constants.soil_moisture_capacity, self.layer_structure.soil_layer_thickness[0], @@ -331,7 +331,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, FunctionalGroup], + functional_groups: dict[str, MicrobialGroup], max_depth_of_microbial_activity: float, soil_moisture_capacity: float, top_soil_layer_thickness: float, From 8e8159eaed9897e02ee1718d5ea66423120208b4 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Thu, 27 Feb 2025 10:11:34 +0000 Subject: [PATCH 6/8] Renamed MicrobialGroup and made it a frozen dataclass --- tests/models/soil/test_microbial_groups.py | 12 ++++---- .../models/soil/microbial_groups.py | 28 ++++++++++--------- virtual_ecosystem/models/soil/pools.py | 8 +++--- virtual_ecosystem/models/soil/soil_model.py | 4 +-- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/models/soil/test_microbial_groups.py b/tests/models/soil/test_microbial_groups.py index 88809f8e0..3a2c6fe63 100644 --- a/tests/models/soil/test_microbial_groups.py +++ b/tests/models/soil/test_microbial_groups.py @@ -8,7 +8,7 @@ def test_make_full_set_of_microbial_groups(): """Test that the function to make all the microbial group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( - MicrobialGroup, + MicrobialGroupConstants, make_full_set_of_microbial_groups, ) @@ -19,7 +19,7 @@ def test_make_full_set_of_microbial_groups(): assert set(expected_groups) == set(functional_groups.keys()) for group in expected_groups: - assert type(functional_groups[group]) is MicrobialGroup + 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 @@ -30,12 +30,12 @@ def test_make_bacterial_functional_group(): """Test that the function to make the bacterial functional group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( - MicrobialGroup, + MicrobialGroupConstants, make_bacterial_functional_group, ) bacterial_group = make_bacterial_functional_group(SoilConsts) - assert type(bacterial_group) is MicrobialGroup + assert type(bacterial_group) is MicrobialGroupConstants # Only testing one value, as testing them all seems like overkill/hard to maintain assert bacterial_group.c_n_ratio == 5.2 @@ -44,11 +44,11 @@ def test_make_fungal_functional_group(): """Test that the function to make the fungal functional group works.""" from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( - MicrobialGroup, + MicrobialGroupConstants, make_fungal_functional_group, ) fungal_group = make_fungal_functional_group(SoilConsts) - assert type(fungal_group) is MicrobialGroup + assert type(fungal_group) is MicrobialGroupConstants # Only testing one value, as testing them all seems like overkill/hard to maintain assert fungal_group.c_n_ratio == 6.5 diff --git a/virtual_ecosystem/models/soil/microbial_groups.py b/virtual_ecosystem/models/soil/microbial_groups.py index 9c8ab016c..8be81ad37 100644 --- a/virtual_ecosystem/models/soil/microbial_groups.py +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -7,9 +7,9 @@ from virtual_ecosystem.models.soil.constants import SoilConsts -@dataclass -class MicrobialGroup: - """Base class for microbial functional groups. +@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. @@ -61,7 +61,7 @@ class MicrobialGroup: def make_full_set_of_microbial_groups( constants: SoilConsts, -) -> dict[str, MicrobialGroup]: +) -> dict[str, MicrobialGroupConstants]: """Make the full set of functional groups used in the soil model. Args: @@ -78,19 +78,20 @@ def make_full_set_of_microbial_groups( } -def make_bacterial_functional_group(constants: SoilConsts) -> MicrobialGroup: +def make_bacterial_functional_group(constants: SoilConsts) -> MicrobialGroupConstants: """Collect the constants for the bacterial functional group. Args: constants: The constants for the soil model. Returns: - A :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroup` object - parameterized with the full set of constants needed to define the bacterial - functional group. + A + :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroupConstants` + object parameterized with the full set of constants needed to define the + bacterial functional group. """ - return MicrobialGroup( + return MicrobialGroupConstants( max_uptake_rate_labile_C=constants.max_bacterial_uptake_rate_labile_C, activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, half_sat_labile_C_uptake=constants.half_sat_bacterial_labile_C_uptake, @@ -108,19 +109,20 @@ def make_bacterial_functional_group(constants: SoilConsts) -> MicrobialGroup: ) -def make_fungal_functional_group(constants: SoilConsts) -> MicrobialGroup: +def make_fungal_functional_group(constants: SoilConsts) -> MicrobialGroupConstants: """Collect the constants for the fungal functional group. Args: constants: The constants for the soil model. Returns: - A :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroup` object - parameterized with the full set of constants needed to define the fungal + A + :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroupConstants` + object parameterized with the full set of constants needed to define the fungal functional group. """ - return MicrobialGroup( + return MicrobialGroupConstants( max_uptake_rate_labile_C=constants.max_fungal_uptake_rate_labile_C, activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, half_sat_labile_C_uptake=constants.half_sat_fungal_labile_C_uptake, diff --git a/virtual_ecosystem/models/soil/pools.py b/virtual_ecosystem/models/soil/pools.py index 53a7d566f..b2ec5424f 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -24,7 +24,7 @@ calculate_symbiotic_nitrogen_fixation_carbon_cost, calculate_temperature_effect_on_microbes, ) -from virtual_ecosystem.models.soil.microbial_groups import MicrobialGroup +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 @@ -289,7 +289,7 @@ def __init__( data: Data, pools: dict[str, NDArray[np.float32]], constants: SoilConsts, - functional_groups: dict[str, MicrobialGroup], + functional_groups: dict[str, MicrobialGroupConstants], max_depth_of_microbial_activity: float, ): self.data = data @@ -667,7 +667,7 @@ def calculate_microbial_changes( soil_temp: NDArray[np.float32], env_factors: EnvironmentalEffectFactors, constants: SoilConsts, - functional_groups: dict[str, MicrobialGroup], + functional_groups: dict[str, MicrobialGroupConstants], ) -> MicrobialChanges: """Calculate the changes for the microbial biomass and enzyme pools. @@ -1076,7 +1076,7 @@ def calculate_nutrient_uptake_rates( pH_factor: NDArray[np.float32], soil_temp: NDArray[np.float32], constants: SoilConsts, - functional_group: MicrobialGroup, + functional_group: MicrobialGroupConstants, ) -> tuple[NDArray[np.float32], NetNutrientConsumption]: """Calculate the rate at which microbes uptake each nutrient. diff --git a/virtual_ecosystem/models/soil/soil_model.py b/virtual_ecosystem/models/soil/soil_model.py index aa178e2a1..9591636a2 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -33,7 +33,7 @@ from virtual_ecosystem.core.logger import LOGGER from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( - MicrobialGroup, + MicrobialGroupConstants, make_full_set_of_microbial_groups, ) from virtual_ecosystem.models.soil.pools import SoilPools @@ -387,7 +387,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, MicrobialGroup], + functional_groups: dict[str, MicrobialGroupConstants], max_depth_of_microbial_activity: float, soil_moisture_capacity: float, top_soil_layer_thickness: float, From d8899b5ecf0a5111e6cef08a34f819f61daf7207 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Fri, 28 Feb 2025 10:55:10 +0000 Subject: [PATCH 7/8] Changed method for generating MicrobialGroupConstants so that they are generated from toml files rather than SoilConsts --- tests/conftest.py | 44 +++- tests/models/soil/conftest.py | 15 +- tests/models/soil/test_microbial_groups.py | 189 ++++++++++++-- tests/models/soil/test_pools.py | 37 +-- tests/models/soil/test_soil_model.py | 24 +- tests/test_main.py | 3 +- .../config/soil_microbial_groups.toml | 33 +++ virtual_ecosystem/models/soil/constants.py | 247 ------------------ .../models/soil/microbial_groups.py | 113 ++++---- .../models/soil/module_schema.json | 75 +++++- virtual_ecosystem/models/soil/pools.py | 36 ++- virtual_ecosystem/models/soil/soil_model.py | 9 +- 12 files changed, 431 insertions(+), 394 deletions(-) create mode 100644 virtual_ecosystem/example_data/config/soil_microbial_groups.toml diff --git a/tests/conftest.py b/tests/conftest.py index 9d4d26d8d..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 diff --git a/tests/models/soil/conftest.py b/tests/models/soil/conftest.py index 61663dea8..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, + ] ) @@ -180,13 +182,10 @@ def maom_desorption(dummy_carbon_data): @pytest.fixture -def functional_groups( - dummy_carbon_data, fixture_core_components, environmental_factors -): +def functional_groups(fixture_config): """Set of functional groups based on the soil model constants.""" - from virtual_ecosystem.models.soil.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( make_full_set_of_microbial_groups, ) - return make_full_set_of_microbial_groups(constants=SoilConsts) + 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 index 3a2c6fe63..80b4638a3 100644 --- a/tests/models/soil/test_microbial_groups.py +++ b/tests/models/soil/test_microbial_groups.py @@ -3,10 +3,16 @@ This module tests the functions which generate microbial functional groups. """ +from logging import CRITICAL -def test_make_full_set_of_microbial_groups(): +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.constants import SoilConsts from virtual_ecosystem.models.soil.microbial_groups import ( MicrobialGroupConstants, make_full_set_of_microbial_groups, @@ -14,7 +20,7 @@ def test_make_full_set_of_microbial_groups(): expected_groups = ["bacteria", "fungi"] - functional_groups = make_full_set_of_microbial_groups(SoilConsts) + functional_groups = make_full_set_of_microbial_groups(fixture_config) assert set(expected_groups) == set(functional_groups.keys()) @@ -26,29 +32,166 @@ def test_make_full_set_of_microbial_groups(): assert functional_groups["fungi"].c_n_ratio == 6.5 -def test_make_bacterial_functional_group(): - """Test that the function to make the bacterial functional group works.""" - from virtual_ecosystem.models.soil.constants import SoilConsts - from virtual_ecosystem.models.soil.microbial_groups import ( - MicrobialGroupConstants, - make_bacterial_functional_group, - ) +@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 - bacterial_group = make_bacterial_functional_group(SoilConsts) - assert type(bacterial_group) is MicrobialGroupConstants - # Only testing one value, as testing them all seems like overkill/hard to maintain - assert bacterial_group.c_n_ratio == 5.2 + [[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 -def test_make_fungal_functional_group(): - """Test that the function to make the fungal functional group works.""" - from virtual_ecosystem.models.soil.constants import SoilConsts + [[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 ( - MicrobialGroupConstants, - make_fungal_functional_group, + make_full_set_of_microbial_groups, ) - fungal_group = make_fungal_functional_group(SoilConsts) - assert type(fungal_group) is MicrobialGroupConstants - # Only testing one value, as testing them all seems like overkill/hard to maintain - assert fungal_group.c_n_ratio == 6.5 + 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 67d37ce5a..1754d215d 100644 --- a/tests/models/soil/test_pools.py +++ b/tests/models/soil/test_pools.py @@ -305,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 ( @@ -319,8 +319,7 @@ def test_calculate_maintenance_biomass_synthesis( soil_temp=dummy_carbon_data["soil_temperature"][ fixture_core_components.layer_structure.index_topsoil_scalar ], - microbial_turnover_rate=SoilConsts.bacterial_turnover_rate, - activation_energy_turnover=SoilConsts.activation_energy_microbial_turnover, + microbial_group=functional_groups["bacteria"], reference_temperature=SoilConsts.arrhenius_reference_temp, ) @@ -422,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 ( @@ -439,10 +438,14 @@ def test_calculate_highest_achievable_nutrient_uptake( soil_temp=dummy_carbon_data["soil_temperature"][ fixture_core_components.layer_structure.index_topsoil_scalar ].to_numpy(), - max_uptake_rate=SoilConsts.max_bacterial_uptake_rate_labile_C, - activation_energy_uptake=SoilConsts.activation_energy_microbial_uptake, - half_saturation_constant=SoilConsts.half_sat_bacterial_labile_C_uptake, - activation_energy_uptake_saturation=SoilConsts.activation_energy_uptake_saturation, + 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, ) @@ -450,7 +453,7 @@ def test_calculate_highest_achievable_nutrient_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 ( @@ -471,10 +474,14 @@ def test_negative_highest_achievable_nutrient_uptake_are_impossible( soil_temp=dummy_carbon_data["soil_temperature"][ fixture_core_components.layer_structure.index_topsoil_scalar ].to_numpy(), - max_uptake_rate=SoilConsts.max_bacterial_uptake_rate_labile_C, - activation_energy_uptake=SoilConsts.activation_energy_microbial_uptake, - half_saturation_constant=SoilConsts.half_sat_bacterial_labile_C_uptake, - activation_energy_uptake_saturation=SoilConsts.activation_energy_uptake_saturation, + 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, ) @@ -642,7 +649,7 @@ def test_calculate_soil_nutrient_mineralisation( assert np.allclose(actual_rate, expected_rate) -def test_calculate_nutrient_flows_to_necromass(): +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, @@ -664,7 +671,7 @@ def test_calculate_nutrient_flows_to_necromass(): bacterial_loss=bacterial_biomass_loss, fungal_loss=fungal_biomass_loss, enzyme_denaturation=enzyme_denaturation, - constants=SoilConsts, + microbial_groups=functional_groups, ) ) diff --git a/tests/models/soil/test_soil_model.py b/tests/models/soil/test_soil_model.py index a24a584e7..7df777a49 100644 --- a/tests/models/soil/test_soil_model.py +++ b/tests/models/soil/test_soil_model.py @@ -48,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 @@ -60,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, ) @@ -122,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 @@ -140,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, ) @@ -154,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 @@ -165,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, ) @@ -182,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(), ( @@ -197,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(), @@ -213,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), @@ -229,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, @@ -245,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() diff --git a/tests/test_main.py b/tests/test_main.py index 76259039f..1985927ba 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -104,6 +104,7 @@ def test_initialise_models( caplog, dummy_carbon_data, + microbial_groups_cfg, cfg_strings, output, raises, @@ -117,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/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/models/soil/constants.py b/virtual_ecosystem/models/soil/constants.py index 195e3a156..fe1cc8678 100644 --- a/virtual_ecosystem/models/soil/constants.py +++ b/virtual_ecosystem/models/soil/constants.py @@ -57,64 +57,6 @@ class SoilConsts(ConstantsDataclass): the source of the activation energies and corresponding rates. """ - max_bacterial_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. - """ - - max_fungal_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 bacterial 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_bacterial_uptake_rate_labile_C`. This activation energy is assumed to be - the same for the uptake of other nutrients as for carbon. - """ - - half_sat_bacterial_labile_C_uptake: float = 0.364 - """Half saturation constant for bacterial 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`. - """ - - half_sat_fungal_labile_C_uptake: float = 0.364 - """Half saturation constant for fungal 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]. @@ -177,35 +119,6 @@ class SoilConsts(ConstantsDataclass): Units of [J K^-1]. Taken from :cite:t:`wang_development_2013`. """ - bacterial_turnover_rate: float = 0.005 - """Bacterial 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 bacterial turnover rate that - this activation energy corresponds to is given by :attr:`bacterial_turnover_rate`. - """ - - fungal_turnover_rate: float = 0.005 - """Fungal 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. - """ - # 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 @@ -389,34 +302,6 @@ class SoilConsts(ConstantsDataclass): leaches from litter solely in organic form. """ - bacterial_c_n_ratio: float = 5.2 - """Ratio of carbon to nitrogen in bacterial biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. - """ - - bacterial_c_p_ratio: float = 16 - """Ratio of carbon to phosphorus in bacterial biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. - """ - - fungal_c_n_ratio: float = 6.5 - """Ratio of carbon to nitrogen in fungal biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. - """ - - fungal_c_p_ratio: float = 40.0 - """Ratio of carbon to phosphorus in fungal biomass [unitless]. - - Estimate taken from :cite:t:`fatichi_mechanistic_2019`, which estimates this based - on previous literature. - """ - ammonium_mineralisation_proportion: float = 0.9 """Proportion of microbially mineralised nitrogen that takes the form of ammonium. @@ -425,138 +310,6 @@ class SoilConsts(ConstantsDataclass): particularly clear. """ - max_bacterial_uptake_rate_ammonium: float = 5e-3 - """Maximum possible rate for bacterial 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_bacterial_ammonium_uptake: float = 0.02275 - """Half saturation constant for bacterial 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_bacterial_uptake_rate_nitrate: float = 5e-4 - """Maximum possible rate for bacterial 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_bacterial_nitrate_uptake: float = 0.02275 - """Half saturation constant for bacterial 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_bacterial_uptake_rate_labile_p: float = 0.0025 - """Maximum possible rate for bacterial 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_bacterial_labile_p_uptake: float = 0.02275 - """Half saturation constant for bacterial 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. - """ - - max_fungal_uptake_rate_ammonium = 5e-3 - """Maximum possible rate for fungal 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_fungal_ammonium_uptake: float = 0.02275 - """Half saturation constant for fungal 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_fungal_uptake_rate_nitrate = 5e-4 - """Maximum possible rate for fungal 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_fungal_nitrate_uptake: float = 0.02275 - """Half saturation constant for fungal 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_fungal_uptake_rate_labile_p: float = 0.0025 - """Maximum possible rate for fungal 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_fungal_labile_p_uptake: float = 0.02275 - """Half saturation constant for fungal 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 index 8be81ad37..e72d39851 100644 --- a/virtual_ecosystem/models/soil/microbial_groups.py +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -4,7 +4,8 @@ from dataclasses import dataclass -from virtual_ecosystem.models.soil.constants import SoilConsts +from virtual_ecosystem.core.config import Config, ConfigurationError +from virtual_ecosystem.core.logger import LOGGER @dataclass(frozen=True) @@ -15,6 +16,9 @@ class MicrobialGroupConstants: 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].""" @@ -60,81 +64,56 @@ class MicrobialGroupConstants: def make_full_set_of_microbial_groups( - constants: SoilConsts, + config: Config, ) -> dict[str, MicrobialGroupConstants]: """Make the full set of functional groups used in the soil model. Args: - constants: The constants for the soil model. + 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). """ + expected_groups = ["fungi", "bacteria"] + + if "soil" not in config: + msg = "Model configuration for soil model not found." + LOGGER.critical(msg) + raise ConfigurationError(msg) + + defined_groups = [ + group["name"] for group in config["soil"]["microbial_group_definition"] + ] + + if set(defined_groups) != set(expected_groups): + if not set(expected_groups).issubset(set(defined_groups)): + msg = ( + "The following expected soil microbial groups are not defined: " + f"{', '.join(set(expected_groups) - set(defined_groups))}" + ) + LOGGER.critical(msg) + if not set(defined_groups).issubset(set(expected_groups)): + msg = ( + "The following microbial groups are not valid: " + f"{', '.join(set(defined_groups) - set(expected_groups))}" + ) + LOGGER.critical(msg) + raise ConfigurationError(msg) + return { - "bacteria": make_bacterial_functional_group(constants), - "fungi": make_fungal_functional_group(constants), + 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 } - - -def make_bacterial_functional_group(constants: SoilConsts) -> MicrobialGroupConstants: - """Collect the constants for the bacterial functional group. - - Args: - constants: The constants for the soil model. - - Returns: - A - :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroupConstants` - object parameterized with the full set of constants needed to define the - bacterial functional group. - """ - - return MicrobialGroupConstants( - max_uptake_rate_labile_C=constants.max_bacterial_uptake_rate_labile_C, - activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, - half_sat_labile_C_uptake=constants.half_sat_bacterial_labile_C_uptake, - activation_energy_uptake_saturation=constants.activation_energy_uptake_saturation, - max_uptake_rate_ammonium=constants.max_bacterial_uptake_rate_ammonium, - half_sat_ammonium_uptake=constants.half_sat_bacterial_ammonium_uptake, - max_uptake_rate_nitrate=constants.max_bacterial_uptake_rate_nitrate, - half_sat_nitrate_uptake=constants.half_sat_bacterial_nitrate_uptake, - max_uptake_rate_labile_p=constants.max_bacterial_uptake_rate_labile_p, - half_sat_labile_p_uptake=constants.half_sat_bacterial_labile_p_uptake, - turnover_rate=constants.bacterial_turnover_rate, - activation_energy_turnover=constants.activation_energy_microbial_turnover, - c_n_ratio=constants.bacterial_c_n_ratio, - c_p_ratio=constants.bacterial_c_p_ratio, - ) - - -def make_fungal_functional_group(constants: SoilConsts) -> MicrobialGroupConstants: - """Collect the constants for the fungal functional group. - - Args: - constants: The constants for the soil model. - - Returns: - A - :class:`~virtual_ecosystem.models.soil.microbial_groups.MicrobialGroupConstants` - object parameterized with the full set of constants needed to define the fungal - functional group. - """ - - return MicrobialGroupConstants( - max_uptake_rate_labile_C=constants.max_fungal_uptake_rate_labile_C, - activation_energy_uptake_rate=constants.activation_energy_microbial_uptake, - half_sat_labile_C_uptake=constants.half_sat_fungal_labile_C_uptake, - activation_energy_uptake_saturation=constants.activation_energy_uptake_saturation, - max_uptake_rate_ammonium=constants.max_fungal_uptake_rate_ammonium, - half_sat_ammonium_uptake=constants.half_sat_fungal_ammonium_uptake, - max_uptake_rate_nitrate=constants.max_fungal_uptake_rate_nitrate, - half_sat_nitrate_uptake=constants.half_sat_fungal_nitrate_uptake, - max_uptake_rate_labile_p=constants.max_fungal_uptake_rate_labile_p, - half_sat_labile_p_uptake=constants.half_sat_fungal_labile_p_uptake, - turnover_rate=constants.fungal_turnover_rate, - activation_energy_turnover=constants.activation_energy_microbial_turnover, - c_n_ratio=constants.fungal_c_n_ratio, - c_p_ratio=constants.fungal_c_p_ratio, - ) 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 b2ec5424f..0be36ab29 100644 --- a/virtual_ecosystem/models/soil/pools.py +++ b/virtual_ecosystem/models/soil/pools.py @@ -718,8 +718,7 @@ def calculate_microbial_changes( bacterial_biomass_loss = calculate_maintenance_biomass_synthesis( microbe_pool_size=soil_c_pool_bacteria, soil_temp=soil_temp, - microbial_turnover_rate=constants.bacterial_turnover_rate, - activation_energy_turnover=constants.activation_energy_microbial_turnover, + microbial_group=functional_groups["bacteria"], reference_temperature=constants.arrhenius_reference_temp, ) fungal_growth, fungal_uptake = calculate_nutrient_uptake_rates( @@ -739,8 +738,7 @@ def calculate_microbial_changes( fungal_biomass_loss = calculate_maintenance_biomass_synthesis( microbe_pool_size=soil_c_pool_fungi, soil_temp=soil_temp, - microbial_turnover_rate=constants.fungal_turnover_rate, - activation_energy_turnover=constants.activation_energy_microbial_turnover, + microbial_group=functional_groups["fungi"], reference_temperature=constants.arrhenius_reference_temp, ) # Find changes in each enzyme pool @@ -771,7 +769,7 @@ def calculate_microbial_changes( bacterial_loss=true_bacterial_loss, fungal_loss=true_fungal_loss, enzyme_denaturation=enzyme_denaturation, - constants=constants, + microbial_groups=functional_groups, ) return MicrobialChanges( @@ -991,8 +989,7 @@ def calculate_enzyme_changes( def calculate_maintenance_biomass_synthesis( microbe_pool_size: NDArray[np.float32], soil_temp: NDArray[np.float32], - microbial_turnover_rate: float, - activation_energy_turnover: float, + microbial_group: MicrobialGroupConstants, reference_temperature: float, ) -> NDArray[np.float32]: """Calculate biomass synthesis rate required to offset losses for a microbial pool. @@ -1004,10 +1001,7 @@ def calculate_maintenance_biomass_synthesis( Args: 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] - microbial_turnover_rate: microbial biomass turnover rate at reference - temperature [day^-1] - activation_energy_turnover: Activation energy for microbial maintenance turnover - rate [J K^-1] + microbial_group: Constants associated with the microbial group of interest reference_temperature: The reference temperature of the Arrhenius equation [C] Returns: @@ -1017,11 +1011,11 @@ def calculate_maintenance_biomass_synthesis( temp_factor = calculate_temperature_effect_on_microbes( soil_temperature=soil_temp, - activation_energy=activation_energy_turnover, + activation_energy=microbial_group.activation_energy_turnover, reference_temperature=reference_temperature, ) - return microbial_turnover_rate * temp_factor * microbe_pool_size + return microbial_group.turnover_rate * temp_factor * microbe_pool_size def calculate_carbon_use_efficiency( @@ -1587,7 +1581,7 @@ def calculate_nutrient_flows_to_necromass( bacterial_loss: NDArray[np.float32], fungal_loss: NDArray[np.float32], enzyme_denaturation: NDArray[np.float32], - constants: SoilConsts, + microbial_groups: dict[str, MicrobialGroupConstants], ) -> tuple[NDArray[np.float32], NDArray[np.float32]]: """Calculate the rate at which nutrients flow into the necromass pool. @@ -1599,7 +1593,7 @@ def calculate_nutrient_flows_to_necromass( 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] - constants: Set of constants for the soil model. + 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 @@ -1611,12 +1605,12 @@ def calculate_nutrient_flows_to_necromass( # model is added (see issue #760) return ( - (bacterial_loss / constants.bacterial_c_n_ratio) - + (fungal_loss / constants.fungal_c_n_ratio) - + (enzyme_denaturation / constants.bacterial_c_n_ratio), - (bacterial_loss / constants.bacterial_c_p_ratio) - + (fungal_loss / constants.fungal_c_p_ratio) - + (enzyme_denaturation / constants.bacterial_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 9591636a2..56c343352 100644 --- a/virtual_ecosystem/models/soil/soil_model.py +++ b/virtual_ecosystem/models/soil/soil_model.py @@ -191,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.""" @@ -209,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 @@ -316,7 +323,7 @@ def integrate(self) -> dict[str, DataArray]: self.layer_structure.index_topsoil_scalar, delta_pools_ordered, self.model_constants, - make_full_set_of_microbial_groups(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], From b606981fd126c2bd364682b0fca54e677aa83537 Mon Sep 17 00:00:00 2001 From: Jacob Cook Date: Fri, 28 Feb 2025 13:12:30 +0000 Subject: [PATCH 8/8] @davidorme's suggestion for improving the display of config errors from make_full_set_of_microbial_groups --- .../models/soil/microbial_groups.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/virtual_ecosystem/models/soil/microbial_groups.py b/virtual_ecosystem/models/soil/microbial_groups.py index e72d39851..db6b9523b 100644 --- a/virtual_ecosystem/models/soil/microbial_groups.py +++ b/virtual_ecosystem/models/soil/microbial_groups.py @@ -81,31 +81,35 @@ def make_full_set_of_microbial_groups( bacteria and fungi). """ - expected_groups = ["fungi", "bacteria"] - if "soil" not in config: msg = "Model configuration for soil model not found." LOGGER.critical(msg) raise ConfigurationError(msg) - defined_groups = [ + expected_groups = {"fungi", "bacteria"} + defined_groups = { group["name"] for group in config["soil"]["microbial_group_definition"] - ] + } - if set(defined_groups) != set(expected_groups): - if not set(expected_groups).issubset(set(defined_groups)): - msg = ( - "The following expected soil microbial groups are not defined: " - f"{', '.join(set(expected_groups) - set(defined_groups))}" - ) - LOGGER.critical(msg) - if not set(defined_groups).issubset(set(expected_groups)): - msg = ( - "The following microbial groups are not valid: " - f"{', '.join(set(defined_groups) - set(expected_groups))}" - ) - LOGGER.critical(msg) - raise ConfigurationError(msg) + 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(