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

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

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

3 

4@author: barna 

5""" 

6from wsimod.core import constants 

7 

8 

9class NutrientPool: 

10 """""" 

11 

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. 

27 

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

49 

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. 

87 

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

98 

99 # Assign parameters 

100 self.temperature_dependence_factor = 0 

101 self.soil_moisture_dependence_factor = 0 

102 

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 ) 

110 

111 self.degrhpar = degrhpar 

112 self.dishpar = dishpar 

113 self.minfpar = minfpar 

114 self.disfpar = disfpar 

115 self.immobdpar = immobdpar 

116 

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 

125 

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 ] 

139 

140 def init_empty(self): 

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

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

143 

144 def init_store(self): 

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

146 self.init_empty() 

147 self.storage = self.get_empty_nutrient() 

148 

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. 

152 

153 Args: 

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

155 the nutrient pool via irrigation 

156 

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 

165 

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. 

169 

170 Args: 

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

172 the nutrient pool via irrigation 

173 

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 

182 

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

186 

187 Args: 

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

189 the nutrient pool via dry deposition 

190 

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 } 

206 

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. 

210 

211 Args: 

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

213 the nutrient pool via wet deposition 

214 

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 

223 

224 def allocate_manure(self, manure): 

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

226 fast pool. 

227 

228 Args: 

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

230 the nutrient pool via manure 

231 

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 ) 

248 

249 def allocate_residue(self, residue): 

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

251 

252 Args: 

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

254 the nutrient pool via residue 

255 

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

270 

271 def allocate_fertiliser(self, fertiliser): 

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

273 and thus updates that pool. 

274 

275 Args: 

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

277 the nutrient pool via fertiliser 

278 

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 

286 

287 def extract_dissolved(self, proportion): 

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

289 

290 Args: 

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

292 

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 ) 

305 

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} 

314 

315 def get_erodable_P(self): 

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

317 inorganic pools). 

318 

319 Returns: 

320 (float): total phosphorus 

321 """ 

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

323 

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. 

327 

328 Args: 

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

330 

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 ) 

339 

340 # Update nutrients in a dict holder 

341 request = self.get_empty_nutrient() 

342 

343 # Update inorganic pool 

344 request["P"] = amount_P * fraction_adsorbed 

345 reply_adsorbed = self.adsorbed_inorganic_pool.extract(request) 

346 

347 # Update humus pool 

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

349 reply_humus = self.humus_pool.extract(request) 

350 

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

352 

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. 

356 

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 

364 

365 # Initialise tracking 

366 increase_in_dissolved_inorganic = self.get_empty_nutrient() 

367 increase_in_dissolved_organic = self.get_empty_nutrient() 

368 

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! 

373 

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 ) 

381 

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 ) 

389 

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 ) 

397 

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? 

406 

407 return increase_in_dissolved_inorganic, increase_in_dissolved_organic 

408 

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. 

412 

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 

419 

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 

439 

440 def get_empty_nutrient(self): 

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

442 

443 Returns: 

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

445 """ 

446 return self.empty_nutrient.copy() 

447 

448 def multiply_nutrients(self, nutrient, factor): 

449 """Multiply nutrients by factors. 

450 

451 Args: 

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

453 factors to multiply for each nutrient 

454 

455 Returns: 

456 (dict): Multiplied nutrients 

457 """ 

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

459 

460 def receive(self, nutrients): 

461 """Update nutrient store by amounts. 

462 

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 

469 

470 def sum_nutrients(self, n1, n2): 

471 """Sum two nutrients. 

472 

473 Args: 

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

475 

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 

483 

484 def subtract_nutrients(self, n1, n2): 

485 """Subtract two nutrients. 

486 

487 Args: 

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

489 to subtract 

490 

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 

498 

499 def extract(self, nutrients): 

500 """Remove nutrients from a store. 

501 

502 Args: 

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

504 

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] 

512 

513 return reply 

514 

515 

516class NutrientStore(NutrientPool): 

517 """""" 

518 

519 def __init__(self): 

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

521 super().init_store()