Skip to content

Commit 3871fb3

Browse files
Merge pull request #672 from ImperialCollegeLondon/663-add-inorganic-phosphorus-pools
Adding inorganic phosphorus pools to the soil model
2 parents 017c70f + 4dc09cb commit 3871fb3

File tree

14 files changed

+806
-185
lines changed

14 files changed

+806
-185
lines changed

docs/source/refs.bib

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,3 +858,17 @@ @article{cleveland_cnp_2007
858858
year = {2007},
859859
pages = {235--252},
860860
}
861+
862+
@article{Mahowald2008,
863+
title = {Global distribution of atmospheric phosphorus sources, concentrations and deposition rates, and anthropogenic impacts},
864+
volume = {22},
865+
issn = {1944-9224},
866+
url = {http://dx.doi.org/10.1029/2008GB003240},
867+
doi = {10.1029/2008gb003240},
868+
number = {4},
869+
journal = {Global Biogeochemical Cycles},
870+
publisher = {American Geophysical Union (AGU)},
871+
author = {Mahowald, Natalie and Jickells, Timothy D. and Baker, Alex R. and Artaxo, Paulo and Benitez‐Nelson, Claudia R. and Bergametti, Gilles and Bond, Tami C. and Chen, Ying and Cohen, David D. and Herut, Barak and Kubilay, Nilgun and Losno, Remi and Luo, Chao and Maenhaut, Willy and McGee, Kenneth A. and Okin, Gregory S. and Siefert, Ronald L. and Tsukuda, Seigen},
872+
year = {2008},
873+
month = dec
874+
}

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ def dummy_carbon_data(fixture_core_components):
269269
"soil_p_pool_particulate": [2.857e-5, 2.85714e-4, 1.142856e-4, 5.714284e-4],
270270
"soil_p_pool_necromass": [0.00080769, 0.00011538, 0.00071538, 0.00044615],
271271
"soil_p_pool_maom": [0.01307692, 0.03461538, 0.01923077, 0.00384615],
272+
"soil_p_pool_primary": [0.0019594, 0.00535662, 0.00277434, 0.00059892],
273+
"soil_p_pool_secondary": [0.00705668, 0.03816896, 0.01152589, 0.00733107],
274+
"soil_p_pool_labile": [1.0582393e-5, 3.252961e-5, 6.806745e-5, 1.945635e-4],
272275
"pH": [3.0, 7.5, 9.0, 5.7],
273276
"bulk_density": [1350.0, 1800.0, 1000.0, 1500.0],
274277
"clay_fraction": [0.8, 0.3, 0.1, 0.9],

tests/core/test_data.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,9 @@ def test_output_current_state(mocker, dummy_carbon_data, time_index):
982982
"soil_p_pool_particulate",
983983
"soil_p_pool_necromass",
984984
"soil_p_pool_maom",
985+
"soil_p_pool_primary",
986+
"soil_p_pool_secondary",
987+
"soil_p_pool_labile",
985988
],
986989
time_index,
987990
)

tests/models/soil/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def microbial_changes(
116116
soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"],
117117
soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"],
118118
soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"],
119+
soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"],
119120
soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"],
120121
soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"],
121122
soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"],

tests/models/soil/test_pools.py

Lines changed: 143 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components):
1313
"""Test that the two pool update functions work correctly."""
14+
from virtual_ecosystem.core.constants import CoreConsts
1415
from virtual_ecosystem.models.soil.pools import SoilPools
1516
from virtual_ecosystem.models.soil.soil_model import SoilModel, make_slices
1617

@@ -33,24 +34,32 @@ def test_calculate_all_pool_updates(dummy_carbon_data, fixture_core_components):
3334
pools = {
3435
str(pool): y0[slc] for slc, pool in zip(slices, delta_pools_ordered.keys())
3536
}
36-
soil_pools = SoilPools(data=dummy_carbon_data, pools=pools, constants=SoilConsts)
37+
soil_pools = SoilPools(
38+
data=dummy_carbon_data,
39+
pools=pools,
40+
constants=SoilConsts,
41+
max_depth_of_microbial_activity=CoreConsts.max_depth_of_microbial_activity,
42+
)
3743

3844
change_in_pools = {
39-
"soil_c_pool_lmwc": [0.01510858, 0.01400719, 0.03697928, 0.02426899],
45+
"soil_c_pool_lmwc": [0.01498062, 0.01332954, 0.03697927, 0.02425546],
4046
"soil_c_pool_maom": [0.038767651, 0.00829848, 0.05982197, 0.07277182],
41-
"soil_c_pool_microbe": [-0.0544059, -0.02282691, -0.11965575, -0.00720166],
47+
"soil_c_pool_microbe": [-0.05435984, -0.02260329, -0.11965575, -0.00719517],
4248
"soil_c_pool_pom": [0.00177803841, -0.007860960795, -0.012016245, 0.00545032],
4349
"soil_c_pool_necromass": [0.001137474, 0.009172067, 0.033573266, -0.08978050],
4450
"soil_enzyme_pom": [1.18e-8, 1.67e-8, 1.8e-9, -1.12e-8],
4551
"soil_enzyme_maom": [-0.00031009, -5.09593e-5, 0.0005990658, -3.72112e-5],
46-
"soil_n_pool_don": [0.00119921, 0.00470261, 0.00496839, 0.00251442],
52+
"soil_n_pool_don": [0.00119058, 0.00466305, 0.00497108, 0.00257023],
4753
"soil_n_pool_particulate": [1.102338e-5, 6.422491e-5, 0.000131687, 1.461799e-5],
4854
"soil_n_pool_necromass": [0.00786114, -0.01209909, 0.00432363, -0.00891218],
4955
"soil_n_pool_maom": [0.00148604, 0.01179891, 0.01365197, 0.0077315],
50-
"soil_p_pool_dop": [1.92918032e-4, 6.24454858e-5, 1.57222238e-4, 9.94118894e-5],
56+
"soil_p_pool_dop": [1.94452573e-4, 7.10041449e-5, 1.86586343e-4, 1.01700974e-4],
5157
"soil_p_pool_particulate": [7.22218e-6, -1.13464e-6, 7.86083e-7, 5.85634364e-7],
5258
"soil_p_pool_necromass": [2.674836e-3, 1.333056e-3, 6.8090685e-3, 4.1429847e-5],
5359
"soil_p_pool_maom": [5.52086672e-4, 3.68566732e-5, 4.7566130e-4, 3.09257058e-4],
60+
"soil_p_pool_primary": [-4.473516e-10, -1.222973e-9, -6.33411e-10, -1.3674e-10],
61+
"soil_p_pool_secondary": [-5.050797e-7, -2.77311e-6, -7.40324e-7, -2.187697e-7],
62+
"soil_p_pool_labile": [-3.851076e-6, -1.965147e-5, -2.749871e-5, -1.591909e-7],
5463
}
5564

5665
# Make order of pools object
@@ -76,18 +85,22 @@ def test_calculate_microbial_changes(
7685

7786
from virtual_ecosystem.models.soil.pools import calculate_microbial_changes
7887

79-
expected_lmwc_uptake = [6.90989514e-5, 4.76229800e-4, 1.55609440e-3, 4.42097002e-5]
80-
expected_don_uptake = [4.78377356e-6, 3.02222758e-5, 8.97746767e-5, 4.08089540e-6]
81-
expected_dop_uptake = [1.55472641e-6, 9.82223962e-6, 2.91767699e-5, 1.32629101e-6]
82-
expected_microbe = [-0.0544059, -0.02282691, -0.11965575, -0.00720166]
83-
expected_pom_enzyme = [1.17571917e-8, 1.67442231e-8, 1.83311362e-9, -1.11675865e-8]
84-
expected_maom_enzyme = [-3.1009224e-4, -5.0959256e-5, 5.9906583e-4, -3.7211168e-5]
85-
expected_necromass = [0.05474086, 0.02303502, 0.11952352, 0.00726011]
88+
expected_mic_changes = {
89+
"lmwc_uptake": [1.97060348e-4, 1.15388472e-3, 1.55610000e-3, 5.77363558e-5],
90+
"don_uptake": [1.36426394e-5, 7.32272994e-5, 8.9775e-5, 5.32950977e-6],
91+
"dop_uptake": [2.25200566e-8, 1.3187241e-6, 8.8919911e-7, 1.3196877e-6],
92+
"labile_p_change": [4.4113378e-6, 2.2480148e-5, 2.8287676e-5, 4.124029e-7],
93+
"microbe_change": [-0.05435984, -0.02260329, -0.11965575, -0.00719517],
94+
"pom_enzyme_change": [1.17571917e-8, 1.6744223e-8, 1.8331136e-9, -1.1167587e-8],
95+
"maom_enzyme_change": [-3.1009224e-4, -5.0959256e-5, 5.990658e-4, -3.721117e-5],
96+
"necromass_generation": [0.05474086, 0.02303502, 0.11952352, 0.00726011],
97+
}
8698

87-
mic_changes = calculate_microbial_changes(
99+
actual_mic_changes = calculate_microbial_changes(
88100
soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"],
89101
soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"],
90102
soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"],
103+
soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"],
91104
soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"],
92105
soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"],
93106
soil_enzyme_maom=dummy_carbon_data["soil_enzyme_maom"],
@@ -98,14 +111,12 @@ def test_calculate_microbial_changes(
98111
constants=SoilConsts,
99112
)
100113

101-
# Check that each rate matches expectation
102-
assert np.allclose(mic_changes.lmwc_uptake, expected_lmwc_uptake)
103-
assert np.allclose(mic_changes.don_uptake, expected_don_uptake)
104-
assert np.allclose(mic_changes.dop_uptake, expected_dop_uptake)
105-
assert np.allclose(mic_changes.microbe_change, expected_microbe)
106-
assert np.allclose(mic_changes.pom_enzyme_change, expected_pom_enzyme)
107-
assert np.allclose(mic_changes.maom_enzyme_change, expected_maom_enzyme)
108-
assert np.allclose(mic_changes.necromass_generation, expected_necromass)
114+
for attr in dir(actual_mic_changes):
115+
if not attr.startswith("_"):
116+
assert attr in expected_mic_changes.keys(), f"Attribute {attr} not tested"
117+
assert np.allclose(
118+
getattr(actual_mic_changes, attr), expected_mic_changes[attr]
119+
)
109120

110121

111122
def test_calculate_enzyme_mediated_rates(
@@ -115,8 +126,10 @@ def test_calculate_enzyme_mediated_rates(
115126

116127
from virtual_ecosystem.models.soil.pools import calculate_enzyme_mediated_rates
117128

118-
expected_pom_to_lmwc = [3.39844565e-4, 8.91990315e-3, 1.25055119e-2, 4.14247999e-5]
119-
expected_maom_to_lmwc = [1.45988485e-3, 2.10172756e-3, 4.69571604e-3, 8.62951373e-6]
129+
expected_rates = {
130+
"pom_to_lmwc": [3.39844565e-4, 8.91990315e-3, 1.25055119e-2, 4.14247999e-5],
131+
"maom_to_lmwc": [1.45988485e-3, 2.10172756e-3, 4.69571604e-3, 8.62951373e-6],
132+
}
120133

121134
actual_rates = calculate_enzyme_mediated_rates(
122135
soil_enzyme_pom=dummy_carbon_data["soil_enzyme_pom"],
@@ -130,8 +143,39 @@ def test_calculate_enzyme_mediated_rates(
130143
constants=SoilConsts,
131144
)
132145

133-
assert np.allclose(actual_rates.pom_to_lmwc, expected_pom_to_lmwc)
134-
assert np.allclose(actual_rates.maom_to_lmwc, expected_maom_to_lmwc)
146+
for attr in dir(actual_rates):
147+
if not attr.startswith("_"):
148+
assert attr in expected_rates.keys(), f"Attribute {attr} not tested"
149+
assert np.allclose(getattr(actual_rates, attr), expected_rates[attr])
150+
151+
152+
def test_calculate_nutrient_leaching(dummy_carbon_data, fixture_core_components):
153+
"""Check that the calculation of dissolved nutrient leaching rates is correct."""
154+
from virtual_ecosystem.models.soil.pools import calculate_nutrient_leaching
155+
156+
expected_leaching = {
157+
"lmwc": [1.0747349e-6, 2.5395235e-6, 9.9154571e-5, 5.2557152e-6],
158+
"don": [1.22826724e-8, 1.81394352e-7, 1.41642304e-7, 3.00326494e-6],
159+
"dop": [1.2282071e-10, 2.90230964e-9, 5.66596981e-8, 1.20130598e-7],
160+
"labile_P": [2.274653e-11, 4.130485e-10, 6.749199e-9, 2.045141e-8],
161+
}
162+
163+
actual_leaching = calculate_nutrient_leaching(
164+
soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"],
165+
soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"],
166+
soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"],
167+
soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"],
168+
vertical_flow_rate=dummy_carbon_data["vertical_flow"].to_numpy(),
169+
soil_moisture=dummy_carbon_data["soil_moisture"][
170+
fixture_core_components.layer_structure.index_topsoil_scalar
171+
].to_numpy(),
172+
constants=SoilConsts,
173+
)
174+
175+
for attr in dir(actual_leaching):
176+
if not attr.startswith("_"):
177+
assert attr in expected_leaching.keys(), f"Attribute {attr} not tested"
178+
assert np.allclose(getattr(actual_leaching, attr), expected_leaching[attr])
135179

136180

137181
def test_calculate_enzyme_changes(dummy_carbon_data):
@@ -232,17 +276,19 @@ def test_calculate_nutrient_uptake_rates(
232276
calculate_nutrient_uptake_rates,
233277
)
234278

235-
expected_carbon_gain = [2.48756225e-5, 1.57155834e-4, 4.66828319e-4, 2.12206561e-5]
279+
expected_carbon_gain = [7.09417251e-5, 3.80781957e-4, 0.00046683, 2.77134508e-5]
236280
expected_consumption_rates = {
237-
"nitrogen": [4.78377356e-6, 3.02222758e-5, 8.97746767e-5, 4.08089540e-6],
238-
"phosphorus": [1.55472641e-6, 9.82223962e-6, 2.91767699e-5, 1.32629101e-6],
239-
"carbon": [6.90989514e-5, 4.76229800e-4, 1.55609440e-3, 4.42097002e-5],
281+
"organic_nitrogen": [1.36426394e-5, 7.32272994e-5, 8.9775e-5, 5.32950977e-6],
282+
"organic_phosphorus": [2.25200566e-8, 1.3187241e-6, 8.8919911e-7, 1.3196877e-6],
283+
"carbon": [1.97060348e-4, 1.15388472e-3, 1.55610000e-3, 5.77363558e-5],
284+
"inorganic_phosphorus": [4.4113378e-6, 2.2480148e-5, 2.8287676e-5, 4.124029e-7],
240285
}
241286

242287
actual_carbon_gain, actual_consumption_rates = calculate_nutrient_uptake_rates(
243288
soil_c_pool_lmwc=dummy_carbon_data["soil_c_pool_lmwc"],
244289
soil_n_pool_don=dummy_carbon_data["soil_n_pool_don"],
245290
soil_p_pool_dop=dummy_carbon_data["soil_p_pool_dop"],
291+
soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"],
246292
soil_c_pool_microbe=dummy_carbon_data["soil_c_pool_microbe"],
247293
water_factor=environmental_factors.water,
248294
pH_factor=environmental_factors.pH,
@@ -254,14 +300,15 @@ def test_calculate_nutrient_uptake_rates(
254300

255301
assert np.allclose(actual_carbon_gain, expected_carbon_gain)
256302

257-
assert set(expected_consumption_rates.keys()) == set(
258-
actual_consumption_rates.keys()
259-
)
260-
261-
for key in expected_consumption_rates.keys():
262-
assert np.allclose(
263-
expected_consumption_rates[key], actual_consumption_rates[key]
264-
)
303+
for attr in dir(actual_consumption_rates):
304+
if not attr.startswith("_"):
305+
assert attr in expected_consumption_rates.keys(), (
306+
f"Attribute {attr} not tested"
307+
)
308+
assert np.allclose(
309+
getattr(actual_consumption_rates, attr),
310+
expected_consumption_rates[attr],
311+
)
265312

266313

267314
def test_calculate_highest_achievable_nutrient_uptake(
@@ -377,26 +424,58 @@ def test_calculate_necromass_breakdown(dummy_carbon_data):
377424
assert np.allclose(actual_breakdown, expected_breakdown)
378425

379426

427+
def test_calculate_litter_mineralisation_fluxes(dummy_carbon_data):
428+
"""Test that calculation of litter mineralisation fluxes works correctly."""
429+
from virtual_ecosystem.models.soil.pools import (
430+
calculate_litter_mineralisation_fluxes,
431+
)
432+
433+
expected_fluxes = {
434+
"lmwc": [3.181590e-6, 1.590795e-6, 7.350000e-7, 8.250000e-6],
435+
"pom": [0.00211788, 0.00105894, 0.00048927, 0.00549175],
436+
"don": [5.302650e-8, 1.060530e-7, 2.745000e-7, 2.449995e-8],
437+
"particulate_n": [3.52979735e-5, 7.05959470e-5, 1.82725500e-4, 1.63088001e-5],
438+
"dop": [7.32000e-10, 1.41404e-10, 2.82808e-10, 6.53332e-11],
439+
"particulate_p": [7.31926800e-6, 1.41389860e-6, 2.82779719e-6, 6.53266667e-7],
440+
"labile_p": [0.0, 0.0, 0.0, 0.0],
441+
}
442+
443+
actual_fluxes = calculate_litter_mineralisation_fluxes(
444+
litter_C_mineralisation_rate=dummy_carbon_data[
445+
"litter_C_mineralisation_rate"
446+
].to_numpy(),
447+
litter_N_mineralisation_rate=dummy_carbon_data[
448+
"litter_N_mineralisation_rate"
449+
].to_numpy(),
450+
litter_P_mineralisation_rate=dummy_carbon_data[
451+
"litter_P_mineralisation_rate"
452+
].to_numpy(),
453+
constants=SoilConsts,
454+
)
455+
456+
# Check all (non-private) dataclass attributes against the dictionary
457+
for attr in dir(actual_fluxes):
458+
if not attr.startswith("_"):
459+
assert attr in expected_fluxes.keys(), f"Attribute {attr} not tested"
460+
assert np.allclose(getattr(actual_fluxes, attr), expected_fluxes[attr])
461+
462+
380463
def test_calculate_litter_mineralisation_split(dummy_carbon_data):
381464
"""Test that the calculation of the mineralisation split works as expected."""
382465
from virtual_ecosystem.models.soil.pools import (
383466
calculate_litter_mineralisation_split,
384467
)
385468

386-
expected_split = {
387-
"dissolved": [3.18159e-6, 1.590795e-6, 7.35e-7, 8.25e-6],
388-
"particulate": [0.00211787841, 0.001058939205, 0.000489265, 0.00549175],
389-
}
469+
expected_dissolved = [3.18159e-6, 1.590795e-6, 7.35e-7, 8.25e-6]
470+
expected_particulate = [0.00211787841, 0.001058939205, 0.000489265, 0.00549175]
390471

391-
actual_split = calculate_litter_mineralisation_split(
472+
actual_particulate, expected_dissolved = calculate_litter_mineralisation_split(
392473
mineralisation_rate=dummy_carbon_data["litter_C_mineralisation_rate"],
393474
litter_leaching_coefficient=SoilConsts.litter_leaching_fraction_carbon,
394475
)
395476

396-
assert set(expected_split.keys()) == set(actual_split.keys())
397-
398-
for key in actual_split.keys():
399-
assert np.allclose(actual_split[key], expected_split[key])
477+
assert np.allclose(actual_particulate, expected_particulate)
478+
assert np.allclose(expected_dissolved, expected_dissolved)
400479

401480

402481
def test_calculate_soil_nutrient_mineralisation(
@@ -493,3 +572,21 @@ def test_calculate_net_nutrient_transfers_from_maom_to_lmwc(
493572

494573
for key in expected_transfers.keys():
495574
assert np.allclose(expected_transfers[key], actual_transfers[key])
575+
576+
577+
def test_calculate_net_formation_of_secondary_P(dummy_carbon_data):
578+
"""Test that calculation of the net formation of secondary P is correct."""
579+
from virtual_ecosystem.models.soil.pools import (
580+
calculate_net_formation_of_secondary_P,
581+
)
582+
583+
expected_formation = [-5.05079715e-7, -2.77311435e-6, -7.4032388e-7, -2.18769722e-7]
584+
585+
actual_formation = calculate_net_formation_of_secondary_P(
586+
soil_p_pool_labile=dummy_carbon_data["soil_p_pool_labile"],
587+
soil_p_pool_secondary=dummy_carbon_data["soil_p_pool_secondary"],
588+
secondary_p_breakdown_rate=SoilConsts.secondary_phosphorus_breakdown_rate,
589+
labile_p_sorption_rate=SoilConsts.labile_phosphorus_sorption_rate,
590+
)
591+
592+
assert np.allclose(actual_formation, expected_formation)

0 commit comments

Comments
 (0)