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