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

1# -*- coding: utf-8 -*- 

2"""Created on Thu May 19 16:42:20 2022. 

3 

4@author: barna 

5""" 

6from typing import Any, Dict 

7 

8from wsimod.core import constants 

9 

10 

11class NutrientPool: 

12 """""" 

13 

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. 

29 

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}. 

51 

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. 

89 

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() 

100 

101 # Assign parameters 

102 self.temperature_dependence_factor = 0 

103 self.soil_moisture_dependence_factor = 0 

104 

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 ) 

112 

113 self.degrhpar = degrhpar 

114 self.dishpar = dishpar 

115 self.minfpar = minfpar 

116 self.disfpar = disfpar 

117 self.immobdpar = immobdpar 

118 

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() 

123 

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 ] 

137 

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 

154 

155 def apply_overrides(self, overrides=Dict[str, Any]): 

156 """Override parameters. 

157 

158 Enables a user to override any of the following parameters: 

159 eto_to_e, pore_depth. 

160 

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", {})) 

180 

181 self.calculate_fraction_parameters() 

182 

183 def init_empty(self): 

184 """Initialise an empty nutrient to be copied.""" 

185 self.empty_nutrient = {x: 0 for x in constants.NUTRIENTS} 

186 

187 def init_store(self): 

188 """Initialise an empty store to track nutrients.""" 

189 self.init_empty() 

190 self.storage = self.get_empty_nutrient() 

191 

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. 

195 

196 Args: 

197 irrigation (dict): A dict that contains the amount of nutrients entering 

198 the nutrient pool via irrigation 

199 

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 

208 

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. 

212 

213 Args: 

214 irrigation (dict): A dict that contains the amount of nutrients entering 

215 the nutrient pool via irrigation 

216 

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 

225 

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). 

229 

230 Args: 

231 deposition (dict): A dict that contains the amount of nutrients entering 

232 the nutrient pool via dry deposition 

233 

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 } 

249 

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. 

253 

254 Args: 

255 deposition (dict): A dict that contains the amount of nutrients entering 

256 the nutrient pool via wet deposition 

257 

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 

266 

267 def allocate_manure(self, manure): 

268 """Assign manure, which is assumed to go to both dissolved inorganic pool and 

269 fast pool. 

270 

271 Args: 

272 manure (dict): A dict that contains the amount of nutrients entering 

273 the nutrient pool via manure 

274 

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 ) 

291 

292 def allocate_residue(self, residue): 

293 """Assign residue, which is assumed to go to both humus pool and fast pool. 

294 

295 Args: 

296 residue (dict): A dict that contains the amount of nutrients entering 

297 the nutrient pool via residue 

298 

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() 

313 

314 def allocate_fertiliser(self, fertiliser): 

315 """Assign fertiliser, which is assumed to contain dissolved inorganic nutrients 

316 and thus updates that pool. 

317 

318 Args: 

319 fertiliser (dict): A dict that contains the amount of nutrients entering 

320 the nutrient pool via fertiliser 

321 

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 

329 

330 def extract_dissolved(self, proportion): 

331 """Function to extract some amount of nutrients from all dissolved pools. 

332 

333 Args: 

334 proportion (float): proportion of the dissolved nutrient pools to extract 

335 

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 ) 

348 

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} 

357 

358 def get_erodable_P(self): 

359 """Return total phosphorus that can be eroded (i.e., humus and adsorbed 

360 inorganic pools). 

361 

362 Returns: 

363 (float): total phosphorus 

364 """ 

365 return self.adsorbed_inorganic_pool.storage["P"] + self.humus_pool.storage["P"] 

366 

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. 

370 

371 Args: 

372 amount_P (float): Amount of phosphorus to be eroded 

373 

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 ) 

382 

383 # Update nutrients in a dict holder 

384 request = self.get_empty_nutrient() 

385 

386 # Update inorganic pool 

387 request["P"] = amount_P * fraction_adsorbed 

388 reply_adsorbed = self.adsorbed_inorganic_pool.extract(request) 

389 

390 # Update humus pool 

391 request["P"] = amount_P * (1 - fraction_adsorbed) 

392 reply_humus = self.humus_pool.extract(request) 

393 

394 return reply_humus["P"], reply_adsorbed["P"] 

395 

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. 

399 

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 

407 

408 # Initialise tracking 

409 increase_in_dissolved_inorganic = self.get_empty_nutrient() 

410 increase_in_dissolved_organic = self.get_empty_nutrient() 

411 

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! 

416 

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 ) 

424 

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 ) 

432 

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 ) 

440 

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? 

449 

450 return increase_in_dissolved_inorganic, increase_in_dissolved_organic 

451 

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. 

455 

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 

462 

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 

482 

483 def get_empty_nutrient(self): 

484 """An efficient way to get an empty nutrient. 

485 

486 Returns: 

487 (dict): A dict containing 0 for each nutrient 

488 """ 

489 return self.empty_nutrient.copy() 

490 

491 def multiply_nutrients(self, nutrient, factor): 

492 """Multiply nutrients by factors. 

493 

494 Args: 

495 nutrient (dict): Dict of nutrients to multiply factor (dict): Dict of 

496 factors to multiply for each nutrient 

497 

498 Returns: 

499 (dict): Multiplied nutrients 

500 """ 

501 return {x: nutrient[x] * factor[x] for x in constants.NUTRIENTS} 

502 

503 def receive(self, nutrients): 

504 """Update nutrient store by amounts. 

505 

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 

512 

513 def sum_nutrients(self, n1, n2): 

514 """Sum two nutrients. 

515 

516 Args: 

517 n1 (dict): Dict of nutrients n2 (dict): Dict of nutrients 

518 

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 

526 

527 def subtract_nutrients(self, n1, n2): 

528 """Subtract two nutrients. 

529 

530 Args: 

531 n1 (dict): Dict of nutrients to subtract from n2 (dict): Dict of nutrients 

532 to subtract 

533 

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 

541 

542 def extract(self, nutrients): 

543 """Remove nutrients from a store. 

544 

545 Args: 

546 nutrients (dict): Dict of nutrients to remove from store 

547 

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] 

555 

556 return reply 

557 

558 

559class NutrientStore(NutrientPool): 

560 """""" 

561 

562 def __init__(self): 

563 """Nutrient store, to be instantiated by a NutrientPool.""" 

564 super().init_store()