Coverage for wsimod\nodes\tanks.py: 35%

184 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-30 14:52 +0000

1"""Module for defining tanks.""" 

2 

3from typing import Any, Dict 

4 

5from wsimod.arcs.arcs import AltQueueArc, DecayArcAlt 

6from wsimod.core import constants 

7from wsimod.core.core import DecayObj, WSIObj 

8 

9 

10class Tank(WSIObj): 

11 """""" 

12 

13 def __init__(self, capacity=0, area=1, datum=10, initial_storage=0): 

14 """A standard storage object. 

15 

16 Args: 

17 capacity (float, optional): Volumetric tank capacity. Defaults to 0. 

18 area (float, optional): Area of tank. Defaults to 1. 

19 datum (float, optional): Datum of tank base (not currently used in any 

20 functions). Defaults to 10. 

21 initial_storage (optional): Initial storage for tank. 

22 float: Tank will be initialised with zero pollutants and the float 

23 as volume 

24 dict: Tank will be initialised with this VQIP 

25 Defaults to 0 (i.e., no volume, no pollutants). 

26 """ 

27 # Set parameters 

28 self.capacity = capacity 

29 self.area = area 

30 self.datum = datum 

31 self.initial_storage = initial_storage 

32 

33 WSIObj.__init__(self) # Not sure why I do this rather than super() 

34 

35 # TODO I don't think the outer if statement is needed 

36 if "initial_storage" in dir(self): 

37 if isinstance(self.initial_storage, dict): 

38 # Assume dict is VQIP describing storage 

39 self.storage = self.copy_vqip(self.initial_storage) 

40 self.storage_ = self.copy_vqip( 

41 self.initial_storage 

42 ) # Lagged storage for mass balance 

43 else: 

44 # Assume number describes initial stroage 

45 self.storage = self.v_change_vqip( 

46 self.empty_vqip(), self.initial_storage 

47 ) 

48 self.storage_ = self.v_change_vqip( 

49 self.empty_vqip(), self.initial_storage 

50 ) # Lagged storage for mass balance 

51 else: 

52 self.storage = self.empty_vqip() 

53 self.storage_ = self.empty_vqip() # Lagged storage for mass balance 

54 

55 def apply_overrides(self, overrides: Dict[str, Any] = {}): 

56 """Apply overrides to the tank. 

57 

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

59 area, capacity, datum. 

60 

61 Args: 

62 overrides (dict, optional): Dictionary of overrides. Defaults to {}. 

63 """ 

64 self.capacity = overrides.pop("capacity", self.capacity) 

65 self.area = overrides.pop("area", self.area) 

66 self.datum = overrides.pop("datum", self.datum) 

67 if len(overrides) > 0: 

68 print(f"No override behaviour defined for: {overrides.keys()}") 

69 

70 def ds(self): 

71 """Should be called by parent object to get change in storage. 

72 

73 Returns: 

74 (dict): Change in storage 

75 """ 

76 return self.ds_vqip(self.storage, self.storage_) 

77 

78 def pull_ponded(self): 

79 """Pull any volume that is above the tank's capacity. 

80 

81 Returns: 

82 ponded (vqip): Amount of ponded water that has been removed from the 

83 tank 

84 

85 Examples: 

86 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

87 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 

88 'phosphate' : 0.2}) 

89 >>> print(my_tank.storage) 

90 {'volume' : 10, 'phosphate' : 0.2} 

91 >>> print(my_tank.pull_ponded()) 

92 {'volume' : 1, 'phosphate' : 0.02} 

93 >>> print(my_tank.storage) 

94 {'volume' : 9, 'phosphate' : 0.18} 

95 """ 

96 # Get amount 

97 ponded = max(self.storage["volume"] - self.capacity, 0) 

98 # Pull from tank 

99 ponded = self.pull_storage({"volume": ponded}) 

100 return ponded 

101 

102 def get_avail(self, vqip=None): 

103 """Get minimum of the amount of water in storage and vqip (if provided). 

104 

105 Args: 

106 vqip (dict, optional): Maximum water required (only 'volume' is used). 

107 Defaults to None. 

108 

109 Returns: 

110 reply (dict): Water available 

111 

112 Examples: 

113 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

114 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 

115 'phosphate' : 0.2}) 

116 >>> print(my_tank.storage) 

117 {'volume' : 10, 'phosphate' : 0.2} 

118 >>> print(my_tank.get_avail()) 

119 {'volume' : 10, 'phosphate' : 0.2} 

120 >>> print(my_tank.get_avail({'volume' : 1})) 

121 {'volume' : 1, 'phosphate' : 0.02} 

122 """ 

123 reply = self.copy_vqip(self.storage) 

124 if vqip is None: 

125 # Return storage 

126 return reply 

127 else: 

128 # Adjust storage pollutants to match volume in vqip 

129 reply = self.v_change_vqip(reply, min(reply["volume"], vqip["volume"])) 

130 return reply 

131 

132 def get_excess(self, vqip=None): 

133 """Get difference between current storage and tank capacity. 

134 

135 Args: 

136 vqip (dict, optional): Maximum capacity required (only 'volume' is 

137 used). Defaults to None. 

138 

139 Returns: 

140 (dict): Difference available 

141 

142 Examples: 

143 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

144 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 

145 'phosphate' : 0.2}) 

146 >>> print(my_tank.get_excess()) 

147 {'volume' : 4, 'phosphate' : 0.16} 

148 >>> print(my_tank.get_excess({'volume' : 2})) 

149 {'volume' : 2, 'phosphate' : 0.08} 

150 """ 

151 vol = max(self.capacity - self.storage["volume"], 0) 

152 if vqip is not None: 

153 vol = min(vqip["volume"], vol) 

154 

155 # Adjust storage pollutants to match volume in vqip 

156 # TODO the v_change_vqip in the reply here is a weird default (if a VQIP is not 

157 # provided) 

158 return self.v_change_vqip(self.storage, vol) 

159 

160 def push_storage(self, vqip, force=False): 

161 """Push water into tank, updating the storage VQIP. Force argument can be used 

162 to ignore tank capacity. 

163 

164 Args: 

165 vqip (dict): VQIP amount to be pushed 

166 force (bool, optional): Argument used to cause function to ignore tank 

167 capacity, possibly resulting in pooling. Defaults to False. 

168 

169 Returns: 

170 reply (dict): A VQIP of water not successfully pushed to the tank 

171 

172 Examples: 

173 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

174 >>> constants.POLLUTANTS = ['phosphate'] 

175 >>> constants.NON_ADDITIVE_POLLUTANTS = [] 

176 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 

177 'phosphate' : 0.2}) 

178 >>> my_push = {'volume' : 10, 'phosphate' : 0.5} 

179 >>> reply = my_tank.push_storage(my_push) 

180 >>> print(reply) 

181 {'volume' : 6, 'phosphate' : 0.3} 

182 >>> print(my_tank.storage) 

183 {'volume': 9.0, 'phosphate': 0.4} 

184 >>> print(my_tank.push_storage(reply, force = True)) 

185 {'phosphate': 0, 'volume': 0} 

186 >>> print(my_tank.storage) 

187 {'volume': 15.0, 'phosphate': 0.7} 

188 """ 

189 if force: 

190 # Directly add request to storage 

191 self.storage = self.sum_vqip(self.storage, vqip) 

192 return self.empty_vqip() 

193 

194 # Check whether request can be met 

195 excess = self.get_excess()["volume"] 

196 

197 # Adjust accordingly 

198 reply = max(vqip["volume"] - excess, 0) 

199 reply = self.v_change_vqip(vqip, reply) 

200 entered = self.v_change_vqip(vqip, vqip["volume"] - reply["volume"]) 

201 

202 # Update storage 

203 self.storage = self.sum_vqip(self.storage, entered) 

204 

205 return reply 

206 

207 def pull_storage(self, vqip): 

208 """Pull water from tank, updating the storage VQIP. Pollutants are removed from 

209 tank in proportion to 'volume' in vqip (pollutant values in vqip are ignored). 

210 

211 Args: 

212 vqip (dict): VQIP amount to be pulled, (only 'volume' key is needed) 

213 

214 Returns: 

215 reply (dict): A VQIP water successfully pulled from the tank 

216 

217 Examples: 

218 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

219 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 

220 'phosphate' : 0.2}) 

221 >>> print(my_tank.pull_storage({'volume' : 6})) 

222 {'volume': 5.0, 'phosphate': 0.2} 

223 >>> print(my_tank.storage) 

224 {'volume': 0, 'phosphate': 0} 

225 """ 

226 # Pull from Tank by volume (taking pollutants in proportion) 

227 if self.storage["volume"] == 0: 

228 return self.empty_vqip() 

229 

230 # Adjust based on available volume 

231 reply = min(vqip["volume"], self.storage["volume"]) 

232 

233 # Update reply to vqip (in proportion to concentration in storage) 

234 reply = self.v_change_vqip(self.storage, reply) 

235 

236 # Extract from storage 

237 self.storage = self.extract_vqip(self.storage, reply) 

238 

239 return reply 

240 

241 def pull_pollutants(self, vqip): 

242 """Pull water from tank, updating the storage VQIP. Pollutants are removed from 

243 tank in according to their values in vqip. 

244 

245 Args: 

246 vqip (dict): VQIP amount to be pulled 

247 

248 Returns: 

249 vqip (dict): A VQIP water successfully pulled from the tank 

250 

251 Examples: 

252 >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] 

253 >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 

254 'phosphate' : 0.2}) 

255 >>> print(my_tank.pull_pollutants({'volume' : 2, 'phosphate' : 0.15})) 

256 {'volume': 2.0, 'phosphate': 0.15} 

257 >>> print(my_tank.storage) 

258 {'volume': 3, 'phosphate': 0.05} 

259 """ 

260 # Adjust based on available mass 

261 for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]: 

262 vqip[pol] = min(self.storage[pol], vqip[pol]) 

263 

264 # Extract from storage 

265 self.storage = self.extract_vqip(self.storage, vqip) 

266 return vqip 

267 

268 def get_head(self, datum=None, non_head_storage=0): 

269 """Area volume calculation for head calcuations. Datum and storage that does not 

270 contribute to head can be specified. 

271 

272 Args: 

273 datum (float, optional): Value to add to pressure head in tank. 

274 Defaults to None. 

275 non_head_storage (float, optional): Amount of storage that does 

276 not contribute to generation of head. The tank must exceed 

277 this value to generate any pressure head. Defaults to 0. 

278 

279 Returns: 

280 head (float): Total head in tank 

281 

282 Examples: 

283 >>> my_tank = Tank(datum = 10, initial_storage = 5, capacity = 10, area = 2) 

284 >>> print(my_tank.get_head()) 

285 12.5 

286 >>> print(my_tank.get_head(non_head_storage = 1)) 

287 12 

288 >>> print(my_tank.get_head(non_head_storage = 1, datum = 0)) 

289 2 

290 """ 

291 # If datum not provided use object datum 

292 if datum is None: 

293 datum = self.datum 

294 

295 # Calculate pressure head generating storage 

296 head_storage = max(self.storage["volume"] - non_head_storage, 0) 

297 

298 # Perform head calculation 

299 head = head_storage / self.area + datum 

300 

301 return head 

302 

303 def evaporate(self, evap): 

304 """Wrapper for v_distill_vqip to apply a volumetric subtraction from tank 

305 storage. Volume removed from storage and no change in pollutant values. 

306 

307 Args: 

308 evap (float): Volume to evaporate 

309 

310 Returns: 

311 evap (float): Volumetric amount of evaporation successfully removed 

312 """ 

313 avail = self.get_avail()["volume"] 

314 

315 evap = min(evap, avail) 

316 self.storage = self.v_distill_vqip(self.storage, evap) 

317 return evap 

318 

319 ##Old function no longer needed (check it is not used anywhere and remove) 

320 def push_total(self, vqip): 

321 """ 

322 

323 Args: 

324 vqip: 

325 

326 Returns: 

327 

328 """ 

329 self.storage = self.sum_vqip(self.storage, vqip) 

330 return self.empty_vqip() 

331 

332 ##Old function no longer needed (check it is not used anywhere and remove) 

333 def push_total_c(self, vqip): 

334 """ 

335 

336 Args: 

337 vqip: 

338 

339 Returns: 

340 

341 """ 

342 # Push vqip to storage where pollutants are given as a concentration rather 

343 # than storage 

344 vqip = self.concentration_to_total(self.vqip) 

345 self.storage = self.sum_vqip(self.storage, vqip) 

346 return self.empty_vqip() 

347 

348 def end_timestep(self): 

349 """Function to be called by parent object, tracks previously timestep's 

350 storage.""" 

351 self.storage_ = self.copy_vqip(self.storage) 

352 

353 def reinit(self): 

354 """Set storage to an empty VQIP.""" 

355 self.storage = self.empty_vqip() 

356 self.storage_ = self.empty_vqip() 

357 

358 

359class ResidenceTank(Tank): 

360 """""" 

361 

362 def __init__(self, residence_time=2, **kwargs): 

363 """A tank that has a residence time property that limits storage pulled from the 

364 'pull_outflow' function. 

365 

366 Args: 

367 residence_time (float, optional): Residence time, in theory given 

368 in timesteps, in practice it just means that storage / 

369 residence time can be pulled each time pull_outflow is called. 

370 Defaults to 2. 

371 """ 

372 self.residence_time = residence_time 

373 super().__init__(**kwargs) 

374 

375 def apply_overrides(self, overrides: Dict[str, Any] = {}): 

376 """Apply overrides to the residencetank. 

377 

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

379 residence_time. 

380 

381 Args: 

382 overrides (dict, optional): Dictionary of overrides. Defaults to {}. 

383 """ 

384 self.residence_time = overrides.pop("residence_time", self.residence_time) 

385 super().apply_overrides(overrides) 

386 

387 def pull_outflow(self): 

388 """Pull storage by residence time from the tank, updating tank storage. 

389 

390 Returns: 

391 outflow (dict): A VQIP with volume of pulled volume and pollutants 

392 proportionate to the tank's pollutants 

393 """ 

394 # Calculate outflow 

395 outflow = self.storage["volume"] / self.residence_time 

396 # Update pollutant amounts 

397 outflow = self.v_change_vqip(self.storage, outflow) 

398 # Remove from tank 

399 outflow = self.pull_storage(outflow) 

400 return outflow 

401 

402 

403class DecayTank(Tank, DecayObj): 

404 """""" 

405 

406 def __init__(self, decays={}, parent=None, **kwargs): 

407 """A tank that has DecayObj functions. Decay occurs in end_timestep, after 

408 updating state variables. In this sense, decay is occurring at the very 

409 beginning of the timestep. 

410 

411 Args: 

412 decays (dict): A dict of dicts containing a key for each pollutant that 

413 decays and, within that, a key for each parameter (a constant and 

414 exponent) 

415 parent (object): An object that can be used to read temperature data from 

416 """ 

417 # Store parameters 

418 self.parent = parent 

419 

420 # Initialise Tank 

421 Tank.__init__(self, **kwargs) 

422 

423 # Initialise decay object 

424 DecayObj.__init__(self, decays) 

425 

426 # Update timestep and ds functions 

427 self.end_timestep = self.end_timestep_decay 

428 self.ds = self.decay_ds 

429 

430 def apply_overrides(self, overrides: Dict[str, Any] = {}): 

431 """Apply overrides to the decaytank. 

432 

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

434 decays. 

435 

436 Args: 

437 overrides (dict, optional): Dictionary of overrides. Defaults to {}. 

438 """ 

439 self.decays.update(overrides.pop("decays", {})) 

440 super().apply_overrides(overrides) 

441 

442 def end_timestep_decay(self): 

443 """Update state variables and call make_decay.""" 

444 self.total_decayed = self.empty_vqip() 

445 self.storage_ = self.copy_vqip(self.storage) 

446 

447 self.storage = self.make_decay(self.storage) 

448 

449 def decay_ds(self): 

450 """Track storage and amount decayed. 

451 

452 Returns: 

453 ds (dict): A VQIP of change in storage and total decayed 

454 """ 

455 ds = self.ds_vqip(self.storage, self.storage_) 

456 ds = self.sum_vqip(ds, self.total_decayed) 

457 return ds 

458 

459 

460class QueueTank(Tank): 

461 """""" 

462 

463 def __init__(self, number_of_timesteps=0, **kwargs): 

464 """A tank with an internal queue arc, whose queue must be completed before 

465 storage is available for use. The storage that has completed the queue is under 

466 the 'active_storage' property. 

467 

468 Args: 

469 number_of_timesteps (int, optional): Built in delay for the internal 

470 queue - it is always added to the queue time, although delay can be 

471 provided with pushes only. Defaults to 0. 

472 """ 

473 # Set parameters 

474 self.number_of_timesteps = number_of_timesteps 

475 

476 super().__init__(**kwargs) 

477 self.end_timestep = self._end_timestep 

478 self.active_storage = self.copy_vqip(self.storage) 

479 

480 # TODO enable queue to be initialised not empty 

481 self.out_arcs = {} 

482 self.in_arcs = {} 

483 # Create internal queue arc 

484 self.internal_arc = AltQueueArc( 

485 in_port=self, out_port=self, number_of_timesteps=self.number_of_timesteps 

486 ) 

487 # TODO should mass balance call internal arc (is this arc called in arc mass 

488 # balance?) 

489 

490 def apply_overrides(self, overrides: Dict[str, Any] = {}): 

491 """Apply overrides to the queuetank. 

492 

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

494 number_of_timesteps. 

495 

496 Args: 

497 overrides (dict, optional): Dictionary of overrides. Defaults to {}. 

498 """ 

499 self.number_of_timesteps = overrides.pop( 

500 "number_of_timesteps", self.number_of_timesteps 

501 ) 

502 self.internal_arc.number_of_timesteps = self.number_of_timesteps 

503 super().apply_overrides(overrides) 

504 

505 def get_avail(self): 

506 """Return the active_storage of the tank. 

507 

508 Returns: 

509 (dict): VQIP of active_storage 

510 """ 

511 return self.copy_vqip(self.active_storage) 

512 

513 def push_storage(self, vqip, time=0, force=False): 

514 """Push storage into QueueTank, applying travel time, unless forced. 

515 

516 Args: 

517 vqip (dict): A VQIP of the amount to push 

518 time (int, optional): Number of timesteps to spend in queue, in addition 

519 to number_of_timesteps property of internal_arc. Defaults to 0. 

520 force (bool, optional): Force property that will ignore tank capacity 

521 and ignore travel time. Defaults to False. 

522 

523 Returns: 

524 reply (dict): A VQIP of water that could not be received by the tank 

525 """ 

526 if force: 

527 # Directly add request to storage, skipping queue 

528 self.storage = self.sum_vqip(self.storage, vqip) 

529 self.active_storage = self.sum_vqip(self.active_storage, vqip) 

530 return self.empty_vqip() 

531 

532 # Push to QueueTank 

533 reply = self.internal_arc.send_push_request(vqip, force=force, time=time) 

534 # Update storage 

535 # TODO storage won't be accurately tracking temperature.. 

536 self.storage = self.sum_vqip( 

537 self.storage, self.v_change_vqip(vqip, vqip["volume"] - reply["volume"]) 

538 ) 

539 return reply 

540 

541 def pull_storage(self, vqip): 

542 """Pull storage from the QueueTank, only water in active_storage is available. 

543 Returning water pulled and updating tank states. Pollutants are removed from 

544 tank in proportion to 'volume' in vqip (pollutant values in vqip are ignored). 

545 

546 Args: 

547 vqip (dict): VQIP amount to pull, only 'volume' property is used 

548 

549 Returns: 

550 reply (dict): VQIP amount that was pulled 

551 """ 

552 # Adjust based on available volume 

553 reply = min(vqip["volume"], self.active_storage["volume"]) 

554 

555 # Update reply to vqip 

556 reply = self.v_change_vqip(self.active_storage, reply) 

557 

558 # Extract from active_storage 

559 self.active_storage = self.extract_vqip(self.active_storage, reply) 

560 

561 # Extract from storage 

562 self.storage = self.extract_vqip(self.storage, reply) 

563 

564 return reply 

565 

566 def pull_storage_exact(self, vqip): 

567 """Pull storage from the QueueTank, only water in active_storage is available. 

568 Pollutants are removed from tank in according to their values in vqip. 

569 

570 Args: 

571 vqip (dict): A VQIP amount to pull 

572 

573 Returns: 

574 reply (dict): A VQIP amount successfully pulled 

575 """ 

576 # Adjust based on available 

577 reply = self.copy_vqip(vqip) 

578 for pol in ["volume"] + constants.ADDITIVE_POLLUTANTS: 

579 reply[pol] = min(reply[pol], self.active_storage[pol]) 

580 

581 # Pull from QueueTank 

582 self.active_storage = self.extract_vqip(self.active_storage, reply) 

583 

584 # Extract from storage 

585 self.storage = self.extract_vqip(self.storage, reply) 

586 return reply 

587 

588 def push_check(self, vqip=None, tag="default"): 

589 """Wrapper for get_excess but applies comparison to volume in VQIP. 

590 Needed to enable use of internal_arc, which assumes it is connecting nodes . 

591 rather than tanks. 

592 NOTE: this is intended only for use with the internal_arc. Pushing to 

593 QueueTanks should use 'push_storage'. 

594 

595 Args: 

596 vqip (dict, optional): VQIP amount to push. Defaults to None. 

597 tag (str, optional): Tag, see Node, don't think it should actually be 

598 used for a QueueTank since there are no handlers. Defaults to 

599 'default'. 

600 

601 Returns: 

602 excess (dict): a VQIP amount of excess capacity 

603 """ 

604 # TODO does behaviour for volume = None need to be defined? 

605 excess = self.get_excess() 

606 if vqip is not None: 

607 excess["volume"] = min(vqip["volume"], excess["volume"]) 

608 return excess 

609 

610 def push_set(self, vqip, tag="default"): 

611 """Behaves differently from normal push setting, it assumes sufficient tank 

612 capacity and receives VQIPs that have reached the END of the internal_arc. 

613 NOTE: this is intended only for use with the internal_arc. Pushing to 

614 QueueTanks should use 'push_storage'. 

615 

616 Args: 

617 vqip (dict): VQIP amount to push 

618 tag (str, optional): Tag, see Node, don't think it should actually be 

619 used for a QueueTank since there are no handlers. Defaults to 

620 'default'. 

621 

622 Returns: 

623 (dict): Returns empty VQIP, indicating all water received (since it 

624 assumes capacity was checked before entering the internal arc) 

625 """ 

626 # Update active_storage (since it has reached the end of the internal_arc) 

627 self.active_storage = self.sum_vqip(self.active_storage, vqip) 

628 

629 return self.empty_vqip() 

630 

631 def _end_timestep(self): 

632 """Wrapper for end_timestep that also ends the timestep in the internal_arc.""" 

633 self.internal_arc.end_timestep() 

634 self.internal_arc.update_queue() 

635 self.storage_ = self.copy_vqip(self.storage) 

636 

637 def reinit(self): 

638 """Zeros storages and arc.""" 

639 self.internal_arc.reinit() 

640 self.storage = self.empty_vqip() 

641 self.storage_ = self.empty_vqip() 

642 self.active_storage = self.empty_vqip() 

643 

644 

645class DecayQueueTank(QueueTank): 

646 """""" 

647 

648 def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs): 

649 """Adds a DecayAltArc in QueueTank to enable decay to occur within the 

650 internal_arc queue. 

651 

652 Args: 

653 decays (dict): A dict of dicts containing a key for each pollutant and, 

654 within that, a key for each parameter (a constant and exponent) 

655 parent (object): An object that can be used to read temperature data from 

656 number_of_timesteps (int, optional): Built in delay for the internal 

657 queue - it is always added to the queue time, although delay can be 

658 provided with pushes only. Defaults to 0. 

659 """ 

660 # Initialise QueueTank 

661 super().__init__(number_of_timesteps=number_of_timesteps, **kwargs) 

662 # Replace internal_arc with a DecayArcAlt 

663 self.internal_arc = DecayArcAlt( 

664 in_port=self, 

665 out_port=self, 

666 number_of_timesteps=number_of_timesteps, 

667 parent=parent, 

668 decays=decays, 

669 ) 

670 

671 self.end_timestep = self._end_timestep 

672 

673 def apply_overrides(self, overrides: Dict[str, Any] = {}): 

674 """Apply overrides to the decayqueuetank. 

675 

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

677 number_of_timesteps, decays. 

678 

679 Args: 

680 overrides (dict, optional): Dictionary of overrides. Defaults to {}. 

681 """ 

682 self.number_of_timesteps = overrides.pop( 

683 "number_of_timesteps", self.number_of_timesteps 

684 ) 

685 self.internal_arc.number_of_timesteps = self.number_of_timesteps 

686 self.internal_arc.decays.update(overrides.pop("decays", {})) 

687 super().apply_overrides(overrides) 

688 

689 def _end_timestep(self): 

690 """End timestep wrapper that removes decayed pollutants and calls internal 

691 arc.""" 

692 # TODO Should the active storage decay if decays are given (probably.. though 

693 # that sounds like a nightmare)? 

694 self.storage = self.extract_vqip(self.storage, self.internal_arc.total_decayed) 

695 self.storage_ = self.copy_vqip(self.storage) 

696 self.internal_arc.end_timestep()