Coverage for wsimod/nodes/nutrient_pool.py: 23%
114 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-01-11 16:39 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-01-11 16:39 +0000
1# -*- coding: utf-8 -*-
2"""Created on Thu May 19 16:42:20 2022.
4@author: barna
5"""
6from wsimod.core import constants
9class NutrientPool:
10 """"""
12 def __init__(
13 self,
14 fraction_dry_n_to_dissolved_inorganic=0.9,
15 degrhpar={"N": 7 * 1e-5, "P": 7 * 1e-6},
16 dishpar={"N": 7 * 1e-5, "P": 7 * 1e-6},
17 minfpar={"N": 0.00013, "P": 0.000003},
18 disfpar={"N": 0.000003, "P": 0.0000001},
19 immobdpar={"N": 0.0056, "P": 0.2866},
20 fraction_manure_to_dissolved_inorganic={"N": 0.5, "P": 0.1},
21 fraction_residue_to_fast={"N": 0.1, "P": 0.1},
22 ):
23 """A class to track nutrient pools in a soil tank, intended to be initialised
24 and called by GrowingSurfaces (see wsimod/nodes/land.py/GrowingSurface) and
25 their subclasses. Contains five pools, which have a storage that tracks the mass
26 of nutrients. Equations and parameters are based on HYPE.
28 Args:
29 fraction_dry_n_to_dissolved_inorganic (float, optional): fraction of dry
30 nitrogen deposition going into the soil dissolved inorganic nitrogen pool,
31 with the rest added to the fast pool. Defaults to 0.9. degrhpar (dict,
32 optional): reference humus degradation rate (fraction of humus pool to fast
33 pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}. dishpar (dict,
34 optional): reference humus dissolution rate (fraction of humus pool to
35 dissolved organic pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}.
36 minfpar (dict, optional): reference fast pool mineralisation rate (fraction
37 of fast pool to dissolved inorganic pool). Defaults to {'N' : 0.00013, 'P' :
38 0.000003}. disfpar (dict, optional): reference fast pool dissolution rate
39 (fraction of fast pool to dissolved organic pool). Defaults to {'N' :
40 0.000003, 'P' : 0.0000001}. immobdpar (dict, optional): reference
41 immobilisation rate (fraction of dissolved inorganic pool to fast pool).
42 Defaults to {'N' : 0.0056, 'P' : 0.2866}.
43 fraction_manure_to_dissolved_inorganic (dict, optional): fraction of
44 nutrients from applied manure to dissolved inorganic pool, with the rest
45 added to the fast pool. Defaults to {'N' : 0.5, 'P' : 0.1}.
46 fraction_residue_to_fast (dict, optional): fraction of nutrients from
47 residue to fast pool, with the rest added to the humus pool. Defaults to
48 {'N' : 0.1, 'P' : 0.1}.
50 Key assumptions:
51 - Four nutrient pools are conceptualised for both nitrogen and phosphorus
52 in soil, which includes humus pool, fast pool, dissolved inorganic pool,
53 and dissolved organic pool. Humus and fast pool represent immobile pool
54 of organic nutrients in the soil with slow and fast turnover,
55 respectively. Dissolved inorganic and organic pool represent nutrients
56 in dissolved phase in soil water (for phosphorus, dissolved organic pool
57 might contain particulate phase). Given that phoshphorus can be adsorbed
58 and attached to soil particles, an adsorbed inorganic pool is created
59 specifically for phosphorus.
60 - The major sources of nutrients to soil are conceptualised as
61 - atmospheric deposition:
62 - dry deposition:
63 - for nitrogen, inorganic fraction of dry deposition is added to
64 the dissovled
65 inorganic pool, while the rest is added to the fast pool;
66 - for phosphorus, all is added to adsorbed inorganic pool.
67 - wet deposition: all is added to the dissolved inorganic pool.
68 - fertilisers: all added to the dissolved inorganic pool.
69 - manure: the inorganic fraction is added to the dissovled inorganic
70 pool, with
71 the rest added to the fast pool.
72 - residue: the part with fast turnover is added to the fast pool, with
73 the rest
74 added to the humus pool.
75 - Nutrient fluxes between these pools are simulated to represent the
76 biochemical processes
77 that can transform the nutrients between different forms. These
78 processes include - degradation of humus pool to fast pool - dissolution
79 of humus pool to dissovled organic pool - mineralisation of fast pool to
80 dissolved inorganic pool - dissolution of fast pool to dissolved organic
81 pool - immobilisation of dissolved inroganic pool to fast pool The rate
82 of these processes are affected by the soil temperature and moisture
83 conditions.
84 - When soil erosion happens, a portion of both the adsorbed inorganic pool
85 and humus pool
86 for phosphorus will be eroded as well.
88 Input data and parameter requirements:
89 - fraction_dry_n_to_dissolved_inorganic,
90 fraction_manure_to_dissolved_inorganic, fraction_residue_to_fast.
91 _Units_: -, all should in [0-1]
92 - degrhpar, dishpar, minfpar, disfpar, immobdpar.
93 _Units_: -, all should in [0-1]
94 """
95 # TODO I don't think anyone will change most of these params... they could maybe
96 # just be set here
97 self.init_empty()
99 # Assign parameters
100 self.temperature_dependence_factor = 0
101 self.soil_moisture_dependence_factor = 0
103 self.fraction_manure_to_dissolved_inorganic = (
104 fraction_manure_to_dissolved_inorganic
105 )
106 self.fraction_residue_to_fast = fraction_residue_to_fast
107 self.fraction_dry_n_to_dissolved_inorganic = (
108 fraction_dry_n_to_dissolved_inorganic
109 )
111 self.degrhpar = degrhpar
112 self.dishpar = dishpar
113 self.minfpar = minfpar
114 self.disfpar = disfpar
115 self.immobdpar = immobdpar
117 self.fraction_manure_to_fast = {
118 x: 1 - self.fraction_manure_to_dissolved_inorganic[x]
119 for x in constants.NUTRIENTS
120 }
121 self.fraction_residue_to_humus = {
122 x: 1 - self.fraction_residue_to_fast[x] for x in constants.NUTRIENTS
123 }
124 self.fraction_dry_n_to_fast = 1 - self.fraction_dry_n_to_dissolved_inorganic
126 # Initialise different pools
127 self.fast_pool = NutrientStore()
128 self.humus_pool = NutrientStore()
129 self.dissolved_inorganic_pool = NutrientStore()
130 self.dissolved_organic_pool = NutrientStore()
131 self.adsorbed_inorganic_pool = NutrientStore()
132 self.pools = [
133 self.fast_pool,
134 self.humus_pool,
135 self.dissolved_inorganic_pool,
136 self.dissolved_organic_pool,
137 self.adsorbed_inorganic_pool,
138 ]
140 def init_empty(self):
141 """Initialise an empty nutrient to be copied."""
142 self.empty_nutrient = {x: 0 for x in constants.NUTRIENTS}
144 def init_store(self):
145 """Initialise an empty store to track nutrients."""
146 self.init_empty()
147 self.storage = self.get_empty_nutrient()
149 def allocate_inorganic_irrigation(self, irrigation):
150 """Assign inorganic irrigation, which is assumed to contain dissolved inorganic
151 nutrients and thus updates that pool.
153 Args:
154 irrigation (dict): A dict that contains the amount of nutrients entering
155 the nutrient pool via irrigation
157 Returns:
158 irrigation (dict): irrigation above, because no transformations take place
159 (i.e., dissolved inorganic is what is received and goes straight into
160 that pool)
161 """
162 # Update pool
163 self.dissolved_inorganic_pool.receive(irrigation)
164 return irrigation
166 def allocate_organic_irrigation(self, irrigation):
167 """Assign organic irrigation, which is assumed to contain dissolved organic
168 nutrients and thus updates that pool.
170 Args:
171 irrigation (dict): A dict that contains the amount of nutrients entering
172 the nutrient pool via irrigation
174 Returns:
175 irrigation (dict): irrigation above, because no transformations take place
176 (i.e., dissolved organic is what is received and goes straight into that
177 pool)
178 """
179 # Update pool
180 self.dissolved_organic_pool.receive(irrigation)
181 return irrigation
183 def allocate_dry_deposition(self, deposition):
184 """Assign dry deposition, which is assumed to go to both dissolved inorganic
185 pool and fast pool (nitrogen) and the adsorbed pool (phosphorus).
187 Args:
188 deposition (dict): A dict that contains the amount of nutrients entering
189 the nutrient pool via dry deposition
191 Returns:
192 (dict): A dict describing the amount of nutrients that enter the nutrient
193 pool in a dissolved form (and thus need to be tracked by the soil water
194 tank)
195 """
196 # Update pools
197 self.fast_pool.storage["N"] += deposition["N"] * self.fraction_dry_n_to_fast
198 self.dissolved_inorganic_pool.storage["N"] += (
199 deposition["N"] * self.fraction_dry_n_to_dissolved_inorganic
200 )
201 self.adsorbed_inorganic_pool.storage["P"] += deposition["P"]
202 return {
203 "N": deposition["N"] * self.fraction_dry_n_to_dissolved_inorganic,
204 "P": 0,
205 }
207 def allocate_wet_deposition(self, deposition):
208 """Assign wet deposition, which is assumed to contain dissolved inorganic
209 nutrients and thus updates that pool.
211 Args:
212 deposition (dict): A dict that contains the amount of nutrients entering
213 the nutrient pool via wet deposition
215 Returns:
216 deposition (dict): deposition above, because no transformations take place
217 (i.e., dissolved inorganic is what is received and goes straight into
218 that pool)
219 """
220 # Update pool
221 self.dissolved_inorganic_pool.receive(deposition)
222 return deposition
224 def allocate_manure(self, manure):
225 """Assign manure, which is assumed to go to both dissolved inorganic pool and
226 fast pool.
228 Args:
229 manure (dict): A dict that contains the amount of nutrients entering
230 the nutrient pool via manure
232 Returns:
233 (dict): A dict describing the amount of nutrients that enter the nutrient
234 pool in a dissolved form (and thus need to be tracked by the soil water
235 tank)
236 """
237 # Assign a proportion of nutrients to the dissolved inorganic pool
238 self.dissolved_inorganic_pool.receive(
239 self.multiply_nutrients(manure, self.fraction_manure_to_dissolved_inorganic)
240 )
241 # Assign a proportion of nutrients to the fast pool
242 self.fast_pool.receive(
243 self.multiply_nutrients(manure, self.fraction_manure_to_fast)
244 )
245 return self.multiply_nutrients(
246 manure, self.fraction_manure_to_dissolved_inorganic
247 )
249 def allocate_residue(self, residue):
250 """Assign residue, which is assumed to go to both humus pool and fast pool.
252 Args:
253 residue (dict): A dict that contains the amount of nutrients entering
254 the nutrient pool via residue
256 Returns:
257 (dict): A dict describing the amount of nutrients that enter the nutrient
258 pool in a dissolved form (and thus need to be tracked by the soil water
259 tank) - i.e., none because fast and humus pool are both solid
260 """
261 # Assign a proportion of nutrients to the humus pool
262 self.humus_pool.receive(
263 self.multiply_nutrients(residue, self.fraction_residue_to_humus)
264 )
265 # Assign a proportion of nutrients to the fast pool
266 self.fast_pool.receive(
267 self.multiply_nutrients(residue, self.fraction_residue_to_fast)
268 )
269 return self.empty_nutrient()
271 def allocate_fertiliser(self, fertiliser):
272 """Assign fertiliser, which is assumed to contain dissolved inorganic nutrients
273 and thus updates that pool.
275 Args:
276 fertiliser (dict): A dict that contains the amount of nutrients entering
277 the nutrient pool via fertiliser
279 Returns:
280 fertiliser (dict): fertiliser above, because no transformations take place
281 (i.e., dissolved inorganic is what is received and goes straight into
282 that pool)
283 """
284 self.dissolved_inorganic_pool.receive(fertiliser)
285 return fertiliser
287 def extract_dissolved(self, proportion):
288 """Function to extract some amount of nutrients from all dissolved pools.
290 Args:
291 proportion (float): proportion of the dissolved nutrient pools to extract
293 Returns:
294 (dict): A dict of dicts, where the top level distinguishes between organic
295 and inorganic nutrients, and the bottom level describes how much
296 nutrients (i.e., N and P) have been extracted from those pools
297 """
298 # Extract from dissolved inorganic pool
299 reply_di = self.dissolved_inorganic_pool.extract(
300 {
301 "N": self.dissolved_inorganic_pool.storage["N"] * proportion,
302 "P": self.dissolved_inorganic_pool.storage["P"] * proportion,
303 }
304 )
306 # Extract from dissolved organic pool
307 reply_do = self.dissolved_organic_pool.extract(
308 {
309 "N": self.dissolved_organic_pool.storage["N"] * proportion,
310 "P": self.dissolved_organic_pool.storage["P"] * proportion,
311 }
312 )
313 return {"organic": reply_do, "inorganic": reply_di}
315 def get_erodable_P(self):
316 """Return total phosphorus that can be eroded (i.e., humus and adsorbed
317 inorganic pools).
319 Returns:
320 (float): total phosphorus
321 """
322 return self.adsorbed_inorganic_pool.storage["P"] + self.humus_pool.storage["P"]
324 def erode_P(self, amount_P):
325 """Update humus and adsorbed inorganic pools to erode some amount. Removed in
326 proportion to amount in both pools.
328 Args:
329 amount_P (float): Amount of phosphorus to be eroded
331 Returns:
332 (float): Amount of phosphorus eroded from the humus pool (float): Amount of
333 phosphorus eroded from the adsorbed inorganic pool
334 """
335 # Calculate proportion of adsorbed to be eroded
336 fraction_adsorbed = self.adsorbed_inorganic_pool.storage["P"] / (
337 self.adsorbed_inorganic_pool.storage["P"] + self.humus_pool.storage["P"]
338 )
340 # Update nutrients in a dict holder
341 request = self.get_empty_nutrient()
343 # Update inorganic pool
344 request["P"] = amount_P * fraction_adsorbed
345 reply_adsorbed = self.adsorbed_inorganic_pool.extract(request)
347 # Update humus pool
348 request["P"] = amount_P * (1 - fraction_adsorbed)
349 reply_humus = self.humus_pool.extract(request)
351 return reply_humus["P"], reply_adsorbed["P"]
353 def soil_pool_transformation(self):
354 """Function to be called by a GrowingSurface that performs and tracks changes
355 resulting from soil transformation processes.
357 Returns:
358 (float): increase in dissolved inorganic nutrients resulting from
359 transformations (negative value indicates a decrease)
360 (float): increase in dissolved organic nutrients resulting from
361 transformations (negative value indicates a decrease)
362 """
363 # For mass balance purposes, assume fast is inorganic and humus is organic
365 # Initialise tracking
366 increase_in_dissolved_inorganic = self.get_empty_nutrient()
367 increase_in_dissolved_organic = self.get_empty_nutrient()
369 # Turnover of humus
370 amount = self.temp_soil_process(self.degrhpar, self.humus_pool, self.fast_pool)
371 # This is solid inorganic to solid organic... no tracking needed since solid
372 # nutrients aren't tracked in mass balance of the surface soil water tank!
374 # Dissolution of humus
375 amount = self.temp_soil_process(
376 self.dishpar, self.humus_pool, self.dissolved_organic_pool
377 )
378 increase_in_dissolved_organic = self.sum_nutrients(
379 increase_in_dissolved_organic, amount
380 )
382 # Turnover of fast
383 amount = self.temp_soil_process(
384 self.minfpar, self.fast_pool, self.dissolved_inorganic_pool
385 )
386 increase_in_dissolved_inorganic = self.sum_nutrients(
387 increase_in_dissolved_inorganic, amount
388 )
390 # Dissolution of fast
391 amount = self.temp_soil_process(
392 self.disfpar, self.fast_pool, self.dissolved_organic_pool
393 )
394 increase_in_dissolved_organic = self.sum_nutrients(
395 increase_in_dissolved_organic, amount
396 )
398 # Immobilisation
399 amount = self.temp_soil_process(
400 self.immobdpar, self.dissolved_inorganic_pool, self.fast_pool
401 )
402 increase_in_dissolved_inorganic = self.subtract_nutrients(
403 increase_in_dissolved_inorganic, amount
404 ) # TODO will a negative value affect the consequent processes in growing
405 # surface?
407 return increase_in_dissolved_inorganic, increase_in_dissolved_organic
409 def temp_soil_process(self, parameter, extract_pool, receive_pool):
410 """Temperature function to take a parameter, calculate transformation, and
411 remove nutrients from the extract pool and update the receive pool.
413 Args:
414 parameter (dict): A dict containing a parameter for each nutrient for the
415 given process
416 (units in per timestep)
417 extract_pool (NutrientStore): The pool to extract from receive_pool
418 (NutrientStore): The pool to receive extracted nutrients
420 Returns:
421 to_extract (dict): A dict containing the amount extracted of each nutrient
422 (for mass
423 balance)
424 """
425 # Initialise nutrients
426 to_extract = self.get_empty_nutrient()
427 for nutrient in constants.NUTRIENTS:
428 # Calculate
429 to_extract[nutrient] = (
430 parameter[nutrient]
431 * self.temperature_dependence_factor
432 * self.soil_moisture_dependence_factor
433 * extract_pool.storage[nutrient]
434 )
435 # Update pools
436 to_extract = extract_pool.extract(to_extract)
437 receive_pool.receive(to_extract)
438 return to_extract
440 def get_empty_nutrient(self):
441 """An efficient way to get an empty nutrient.
443 Returns:
444 (dict): A dict containing 0 for each nutrient
445 """
446 return self.empty_nutrient.copy()
448 def multiply_nutrients(self, nutrient, factor):
449 """Multiply nutrients by factors.
451 Args:
452 nutrient (dict): Dict of nutrients to multiply factor (dict): Dict of
453 factors to multiply for each nutrient
455 Returns:
456 (dict): Multiplied nutrients
457 """
458 return {x: nutrient[x] * factor[x] for x in constants.NUTRIENTS}
460 def receive(self, nutrients):
461 """Update nutrient store by amounts.
463 Args:
464 nutrients (dict): Amount of nutrients to update store by
465 """
466 # Increase storage
467 for nutrient, amount in nutrients.items():
468 self.storage[nutrient] += amount
470 def sum_nutrients(self, n1, n2):
471 """Sum two nutrients.
473 Args:
474 n1 (dict): Dict of nutrients n2 (dict): Dict of nutrients
476 Returns:
477 (dict): Summed nutrients
478 """
479 reply = self.get_empty_nutrient()
480 for nutrient in constants.NUTRIENTS:
481 reply[nutrient] = n1[nutrient] + n2[nutrient]
482 return reply
484 def subtract_nutrients(self, n1, n2):
485 """Subtract two nutrients.
487 Args:
488 n1 (dict): Dict of nutrients to subtract from n2 (dict): Dict of nutrients
489 to subtract
491 Returns:
492 (dict): subtracted nutrients
493 """
494 reply = self.get_empty_nutrient()
495 for nutrient in constants.NUTRIENTS:
496 reply[nutrient] = n1[nutrient] - n2[nutrient]
497 return reply
499 def extract(self, nutrients):
500 """Remove nutrients from a store.
502 Args:
503 nutrients (dict): Dict of nutrients to remove from store
505 Returns:
506 (dict): amount of nutrients successfully removed
507 """
508 reply = self.get_empty_nutrient()
509 for nutrient, amount in nutrients.items():
510 reply[nutrient] = min(self.storage[nutrient], amount)
511 self.storage[nutrient] -= reply[nutrient]
513 return reply
516class NutrientStore(NutrientPool):
517 """"""
519 def __init__(self):
520 """Nutrient store, to be instantiated by a NutrientPool."""
521 super().init_store()