Coverage for wsimod/nodes/sewer.py: 19%

72 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-01-11 16:39 +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 wsimod.core import constants 

8from wsimod.nodes.nodes import Node, QueueTank 

9 

10 

11class Sewer(Node): 

12 """""" 

13 

14 def __init__( 

15 self, 

16 name, 

17 capacity=0, 

18 pipe_time=0, # Sewer to sewer travel time 

19 pipe_timearea={0: 1}, 

20 chamber_area=1, 

21 chamber_floor=10, 

22 data_input_dict={}, 

23 ): 

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

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

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

27 

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

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

30 uniformly over the day). 

31 

32 Args: 

33 name (str): node name 

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

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

36 the sewer tank. Defaults to 0. 

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

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

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

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

41 0 timesteps and 30% takes 1 timesteps. 

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

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

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

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

46 

47 NOTE that currently the queuetank either applies the pipe_timearea 

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

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

50 the sewer_tank and removing the pipe_time setting in push_set_sewer. 

51 

52 Functions intended to call in orchestration: 

53 make_discharge 

54 

55 Key assumptions: 

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

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

58 in a single component. 

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

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

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

62 method. 

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

64 travel time through the node. 

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

66 `Tank`. 

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

68 

69 Input data and parameter requirements: 

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

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

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

73 `Sewer` 

74 objects. 

75 _Units_: number of timesteps 

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

77 `Tank` that controls flow. 

78 _Units_: cubic metres, squared metres, metres 

79 """ 

80 # Set parameters 

81 self.capacity = capacity 

82 self.pipe_time = pipe_time 

83 self.pipe_timearea = pipe_timearea 

84 self.chamber_area = chamber_area 

85 self.chamber_floor = chamber_floor 

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

87 self.data_input_dict = data_input_dict 

88 

89 # Update args 

90 super().__init__(name) 

91 

92 # Update handlers 

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

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

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

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

97 

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

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

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

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

102 

103 # Create sewer tank 

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

105 self.sewer_tank = QueueTank( 

106 capacity=self.capacity, 

107 number_of_timesteps=0, 

108 datum=self.chamber_floor, 

109 area=self.chamber_area, 

110 ) 

111 

112 # Mass balance 

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

114 

115 def push_check_sewer(self, vqip=None): 

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

117 

118 Args: 

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

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

121 

122 Returns: 

123 excess (dict): Sewer tank excess 

124 """ 

125 # Get excess 

126 excess = self.sewer_tank.get_excess() 

127 if vqip is None: 

128 return excess 

129 # Limit respone to vqip volume 

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

131 return excess 

132 

133 def push_set_sewer(self, vqip): 

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

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

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

137 thus the water is forced. 

138 

139 Args: 

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

141 

142 Returns: 

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

144 """ 

145 # Sewer to sewer push, update queued tank 

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

147 

148 def push_set_land(self, vqip): 

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

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

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

152 

153 Args: 

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

155 

156 Returns: 

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

158 """ 

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

160 

161 reply = self.empty_vqip() 

162 

163 # Iterate over timearea diagram 

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

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

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

167 reply = self.sum_vqip(reply, reply_) 

168 

169 return reply 

170 

171 def make_discharge(self): 

172 """Function to trigger downstream sewer flow. 

173 

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

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

176 """ 

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

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

179 # Discharge to Sewer if possible 

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

181 # of_type = 'Sewer', 

182 # tag = 'Sewer') 

183 

184 # #Discharge to WWTW if possible 

185 # remaining = self.push_distributed(remaining, 

186 # of_type = 'WWTW', 

187 # tag = 'Sewer') 

188 

189 # #CSO discharge 

190 # remaining = self.push_distributed(remaining, 

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

192 

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

194 

195 # TODO backflow can cause mass balance errors here 

196 

197 # Update tank 

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

199 reply = self.sewer_tank.pull_storage_exact(sent) 

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

201 print("Miscalculated tank storage in discharge") 

202 

203 # Flood excess 

204 ponded = self.sewer_tank.pull_ponded() 

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

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

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

208 if reply_["volume"]: 

209 print("ponded water cant reenter") 

210 

211 def end_timestep(self): 

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

213 self.sewer_tank.end_timestep() 

214 

215 def reinit(self): 

216 """Call Tank reinit.""" 

217 self.sewer_tank.reinit() 

218 

219 

220class EnfieldFoulSewer(Sewer): 

221 """""" 

222 

223 # TODO: combine with sewer 

224 def __init__( 

225 self, 

226 name, 

227 capacity=0, 

228 pipe_time=0, # Sewer to sewer travel time 

229 pipe_timearea={0: 1}, 

230 chamber_area=1, 

231 chamber_floor=10, 

232 data_input_dict={}, 

233 ): 

234 """Alternate legacy sewer class... 

235 

236 I dont think this is needed any more. 

237 """ 

238 # TODO above 

239 

240 super().__init__( 

241 name, 

242 capacity=capacity, 

243 pipe_time=pipe_time, 

244 pipe_timearea=pipe_timearea, 

245 chamber_area=chamber_area, 

246 chamber_floor=chamber_floor, 

247 data_input_dict=data_input_dict, 

248 ) 

249 self.__class__.__name__ = "Sewer" 

250 

251 def make_discharge(self): 

252 """""" 

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

254 

255 # Discharge downstream 

256 if ( 

257 self.sewer_tank.storage["volume"] 

258 > self.storm_exchange * self.sewer_tank.capacity 

259 ): 

260 exchange_v = min( 

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

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

263 ) 

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

265 remaining = self.push_distributed(exchange) 

266 sent_to_exchange = self.v_change_vqip( 

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

268 ) 

269 self.sewer_tank.pull_storage(sent_to_exchange) 

270 

271 remaining = self.push_distributed( 

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

273 ) 

274 

275 # Update tank 

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

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

278 reply = self.sewer_tank.pull_storage(sent) 

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

280 print("Miscalculated tank storage in discharge")