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
« 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.
4@author: bdobson
5Converted to totals on 2022-05-03
6"""
7from typing import Any, Dict
9from wsimod.core import constants
10from wsimod.nodes.nodes import Node
11from wsimod.nodes.tanks import QueueTank
14class Sewer(Node):
15 """"""
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.
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).
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 {}.
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.
55 Functions intended to call in orchestration:
56 make_discharge
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.
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
92 # Update args
93 super().__init__(name)
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
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
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 )
115 # Mass balance
116 self.mass_balance_ds.append(lambda: self.sewer_tank.ds())
118 def apply_overrides(self, overrides: Dict[str, Any] = {}):
119 """Apply overrides to the sewer.
121 Enables a user to override any of the following parameters:
122 capacity, chamber_area, chamber_floor, pipe_time, pipe_timearea.
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
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)
145 def push_check_sewer(self, vqip=None):
146 """Generic push check, simply looks at excess.
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.
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
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.
169 Args:
170 vqip (dict): A VQIP amount of water to push
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)
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.
183 Args:
184 vqip (dict): A VQIP amount to be pushed
186 Returns:
187 (dict): A VQIP amount that was not received
188 """
189 # Land/demand to sewer push, update queued tank
191 reply = self.empty_vqip()
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_)
199 return reply
201 def make_discharge(self):
202 """Function to trigger downstream sewer flow.
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')
214 # #Discharge to WWTW if possible
215 # remaining = self.push_distributed(remaining,
216 # of_type = 'WWTW',
217 # tag = 'Sewer')
219 # #CSO discharge
220 # remaining = self.push_distributed(remaining,
221 # of_type = ['Node', 'River'])
223 remaining = self.push_distributed(self.sewer_tank.active_storage)
225 # TODO backflow can cause mass balance errors here
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")
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")
241 def end_timestep(self):
242 """Overwrite end_timestep behaviour to update tank variables."""
243 self.sewer_tank.end_timestep()
245 def reinit(self):
246 """Call Tank reinit."""
247 self.sewer_tank.reinit()
250class EnfieldFoulSewer(Sewer):
251 """"""
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...
266 I dont think this is needed any more.
267 """
268 # TODO above
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"
281 def make_discharge(self):
282 """"""
283 _ = self.sewer_tank.internal_arc.update_queue(direction="push")
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)
301 remaining = self.push_distributed(
302 self.sewer_tank.active_storage, of_type=["Waste"]
303 )
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")