Coverage for wsimod\nodes\sewer.py: 52%

87 statements  

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

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

2"""Created on Mon Nov 15 14:20:36 2021. 

3 

4@author: bdobson 

5Converted to totals on 2022-05-03 

6""" 

7from typing import Any, Dict 

8 

9from wsimod.core import constants 

10from wsimod.nodes.nodes import Node 

11from wsimod.nodes.tanks import QueueTank 

12 

13 

14class Sewer(Node): 

15 """""" 

16 

17 def __init__( 

18 self, 

19 name, 

20 capacity=0, 

21 pipe_time=0, # Sewer to sewer travel time 

22 pipe_timearea={0: 1}, 

23 chamber_area=1, 

24 chamber_floor=10, 

25 data_input_dict={}, 

26 ): 

27 """Sewer node that has a QueueTank and storage capacity. Think carefully about 

28 parameterising this tank, because of course the amount of water that can flow 

29 through a sewer in a timestep is different in reality than in a. 

30 

31 steady state (e.g., a sewer that can handle a peak of 6m3/s in practice could 

32 not handle 6 * 86400 m3 of water in a day because that water does not flow 

33 uniformly over the day). 

34 

35 Args: 

36 name (str): node name 

37 capacity (float, optional): Sewer tank capacity. Defaults to 0. 

38 pipe_time (float, optional): Number of timesteps to spend in the queue of 

39 the sewer tank. Defaults to 0. 

40 pipe_timearea (dict, optional): Time area diagram that enables flows to 

41 take a range of different durations to 'traverse' the tank. The keys 

42 of the dict are the number of timesteps while the values are the 

43 proportion of flow. E.g., {0 : 0.7, 1 : 0.3} means 70% of flow takes 

44 0 timesteps and 30% takes 1 timesteps. 

45 chamber_area (float, optional): Sewer tank area. Defaults to 1. 

46 chamber_floor (float, optional): Sewer tank datum. Defaults to 10. 

47 data_input_dict (dict, optional): Dictionary of data inputs relevant for 

48 the node (though I don't think it is used). Defaults to {}. 

49 

50 NOTE that currently the queuetank either applies the pipe_timearea 

51 (push_set_land) OR the pipe_time (push_set_sewer). Though this behaviour 

52 could be changed by setting the number_of_timesteps property to pipe_time of 

53 the sewer_tank and removing the pipe_time setting in push_set_sewer. 

54 

55 Functions intended to call in orchestration: 

56 make_discharge 

57 

58 Key assumptions: 

59 - Sewer networks can be represented in an aggregated manner, where 

60 the behaviour of collections of manholes/pipes can be captured 

61 in a single component. 

62 - Travel time of water received from either `land.py/Land` objects 

63 or `demand.py/Demand` objects is assumed to be received as a 

64 non-point source and thus can be represented with the time-area 

65 method. 

66 - Travel time of water from an upstream `Sewer` object has a fixed 

67 travel time through the node. 

68 - The flow capacity of sewer network can be represented as with a 

69 `Tank`. 

70 - The `Sewer` object is not currently biochemically active. 

71 

72 Input data and parameter requirements: 

73 - `pipe_timearea` is a dictionary containing the timearea diagram. 

74 _Units_: duration of flow (in timesteps) and proportion of flow 

75 - `pipe_time` describes the travel time of water received from upstream 

76 `Sewer` 

77 objects. 

78 _Units_: number of timesteps 

79 - `capacity`, `chamber_area`, `chamber_datum` describe the dimensions of the 

80 `Tank` that controls flow. 

81 _Units_: cubic metres, squared metres, metres 

82 """ 

83 # Set parameters 

84 self.capacity = capacity 

85 self.pipe_time = pipe_time 

86 self.pipe_timearea = pipe_timearea 

87 self.chamber_area = chamber_area 

88 self.chamber_floor = chamber_floor 

89 # TODO I don't think this is used.. 

90 self.data_input_dict = data_input_dict 

91 

92 # Update args 

93 super().__init__(name) 

94 

95 # Update handlers 

96 self.push_set_handler["Sewer"] = self.push_set_sewer 

97 self.push_set_handler["default"] = self.push_set_sewer 

98 self.push_set_handler["Land"] = self.push_set_land 

99 self.push_set_handler["Demand"] = self.push_set_land 

100 

101 self.push_check_handler["default"] = self.push_check_sewer 

102 self.push_check_handler["Sewer"] = self.push_check_sewer 

103 self.push_check_handler["Demand"] = self.push_check_sewer 

104 self.push_check_handler["Land"] = self.push_check_sewer 

105 

106 # Create sewer tank 

107 # TODO this might work better as a ResidenceTank (maybe also decay?) 

108 self.sewer_tank = QueueTank( 

109 capacity=self.capacity, 

110 number_of_timesteps=0, 

111 datum=self.chamber_floor, 

112 area=self.chamber_area, 

113 ) 

114 

115 # Mass balance 

116 self.mass_balance_ds.append(lambda: self.sewer_tank.ds()) 

117 

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

119 """Apply overrides to the sewer. 

120 

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

122 capacity, chamber_area, chamber_floor, pipe_time, pipe_timearea. 

123 

124 Args: 

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

126 """ 

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

128 self.chamber_area = overrides.pop("chamber_area", self.chamber_area) 

129 self.chamber_floor = overrides.pop("chamber_floor", self.chamber_floor) 

130 self.sewer_tank.capacity = self.capacity 

131 self.sewer_tank.area = self.chamber_area 

132 self.sewer_tank.datum = self.chamber_floor 

133 

134 self.pipe_time = overrides.pop("pipe_time", self.pipe_time) 

135 if "pipe_timearea" in overrides.keys(): 

136 pipe_timearea_sum = sum([v for k, v in overrides["pipe_timearea"].items()]) 

137 if pipe_timearea_sum != 1: 

138 print( 

139 "ERROR: the sum of pipe_timearea in the overrides dict \ 

140 is not equal to 1, please check it" 

141 ) 

142 self.pipe_timearea = overrides.pop("pipe_timearea", self.pipe_timearea) 

143 super().apply_overrides(overrides) 

144 

145 def push_check_sewer(self, vqip=None): 

146 """Generic push check, simply looks at excess. 

147 

148 Args: 

149 vqip (dict, optional): A VQIP that can be used to limit the volume in 

150 the return value (only volume key is used). Defaults to None. 

151 

152 Returns: 

153 excess (dict): Sewer tank excess 

154 """ 

155 # Get excess 

156 excess = self.sewer_tank.get_excess() 

157 if vqip is None: 

158 return excess 

159 # Limit respone to vqip volume 

160 excess = self.v_change_vqip(excess, min(excess["volume"], vqip["volume"])) 

161 return excess 

162 

163 def push_set_sewer(self, vqip): 

164 """Generic push request setting that implements basic queue travel time (it does 

165 NOT implement timearea travel time). Updates the sewer tank storage. Assumes 

166 that the inflow arc has accurately calculated capacity with push_check_sewer, 

167 thus the water is forced. 

168 

169 Args: 

170 vqip (dict): A VQIP amount of water to push 

171 

172 Returns: 

173 (dict): A VQIP amount of water that was not received 

174 """ 

175 # Sewer to sewer push, update queued tank 

176 return self.sewer_tank.push_storage(vqip, time=self.pipe_time) 

177 

178 def push_set_land(self, vqip): 

179 """Push request that applies pipe_timearea (see __init__ for description). As 

180 with push_set_sewer, push is also forced. Used to receive flow from land or 

181 demand that is assumed to occur widely across some kind of sewer catchment. 

182 

183 Args: 

184 vqip (dict): A VQIP amount to be pushed 

185 

186 Returns: 

187 (dict): A VQIP amount that was not received 

188 """ 

189 # Land/demand to sewer push, update queued tank 

190 

191 reply = self.empty_vqip() 

192 

193 # Iterate over timearea diagram 

194 for time, normalised in self.pipe_timearea.items(): 

195 vqip_ = self.v_change_vqip(vqip, vqip["volume"] * normalised) 

196 reply_ = self.sewer_tank.push_storage(vqip_, time=time) 

197 reply = self.sum_vqip(reply, reply_) 

198 

199 return reply 

200 

201 def make_discharge(self): 

202 """Function to trigger downstream sewer flow. 

203 

204 Updates sewer tank travel time, pushes to WWTW, then sewer, then CSO. May flood 

205 land if, after these attempts, the sewer tank storage is above capacity. 

206 """ 

207 self.sewer_tank.internal_arc.update_queue(direction="push") 

208 # TODO... do I need to do anything with this backflow... does it ever happen? 

209 # Discharge to Sewer if possible 

210 # remaining = self.push_distributed(self.sewer_tank.active_storage, 

211 # of_type = 'Sewer', 

212 # tag = 'Sewer') 

213 

214 # #Discharge to WWTW if possible 

215 # remaining = self.push_distributed(remaining, 

216 # of_type = 'WWTW', 

217 # tag = 'Sewer') 

218 

219 # #CSO discharge 

220 # remaining = self.push_distributed(remaining, 

221 # of_type = ['Node', 'River']) 

222 

223 remaining = self.push_distributed(self.sewer_tank.active_storage) 

224 

225 # TODO backflow can cause mass balance errors here 

226 

227 # Update tank 

228 sent = self.extract_vqip(self.sewer_tank.active_storage, remaining) 

229 reply = self.sewer_tank.pull_storage_exact(sent) 

230 if (reply["volume"] - sent["volume"]) > constants.FLOAT_ACCURACY: 

231 print("Miscalculated tank storage in discharge") 

232 

233 # Flood excess 

234 ponded = self.sewer_tank.pull_ponded() 

235 if ponded["volume"] > constants.FLOAT_ACCURACY: 

236 reply_ = self.push_distributed(ponded, of_type=["Land"], tag="Sewer") 

237 reply_ = self.sewer_tank.push_storage(reply_, time=0, force=True) 

238 if reply_["volume"]: 

239 print("ponded water cant reenter") 

240 

241 def end_timestep(self): 

242 """Overwrite end_timestep behaviour to update tank variables.""" 

243 self.sewer_tank.end_timestep() 

244 

245 def reinit(self): 

246 """Call Tank reinit.""" 

247 self.sewer_tank.reinit() 

248 

249 

250class EnfieldFoulSewer(Sewer): 

251 """""" 

252 

253 # TODO: combine with sewer 

254 def __init__( 

255 self, 

256 name, 

257 capacity=0, 

258 pipe_time=0, # Sewer to sewer travel time 

259 pipe_timearea={0: 1}, 

260 chamber_area=1, 

261 chamber_floor=10, 

262 data_input_dict={}, 

263 ): 

264 """Alternate legacy sewer class... 

265 

266 I dont think this is needed any more. 

267 """ 

268 # TODO above 

269 

270 super().__init__( 

271 name, 

272 capacity=capacity, 

273 pipe_time=pipe_time, 

274 pipe_timearea=pipe_timearea, 

275 chamber_area=chamber_area, 

276 chamber_floor=chamber_floor, 

277 data_input_dict=data_input_dict, 

278 ) 

279 self.__class__.__name__ = "Sewer" 

280 

281 def make_discharge(self): 

282 """""" 

283 _ = self.sewer_tank.internal_arc.update_queue(direction="push") 

284 

285 # Discharge downstream 

286 if ( 

287 self.sewer_tank.storage["volume"] 

288 > self.storm_exchange * self.sewer_tank.capacity 

289 ): 

290 exchange_v = min( 

291 (1 - self.storm_exchange) * self.sewer_tank.capacity, 

292 self.sewer_tank.active_storage["volume"], 

293 ) 

294 exchange = self.v_change_vqip(self.sewer_tank.active_storage, exchange_v) 

295 remaining = self.push_distributed(exchange) 

296 sent_to_exchange = self.v_change_vqip( 

297 self.sewer_tank.active_storage, exchange_v - remaining["volume"] 

298 ) 

299 self.sewer_tank.pull_storage(sent_to_exchange) 

300 

301 remaining = self.push_distributed( 

302 self.sewer_tank.active_storage, of_type=["Waste"] 

303 ) 

304 

305 # Update tank 

306 sent = self.sewer_tank.active_storage["volume"] - remaining["volume"] 

307 sent = self.v_change_vqip(self.sewer_tank.active_storage, sent) 

308 reply = self.sewer_tank.pull_storage(sent) 

309 if (reply["volume"] - sent["volume"]) > constants.FLOAT_ACCURACY: 

310 print("Miscalculated tank storage in discharge")