epyt-flow 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/EPANET/compile_macos.sh +4 -0
- epyt_flow/VERSION +1 -1
- epyt_flow/__init__.py +6 -2
- epyt_flow/data/benchmarks/leakdb.py +7 -12
- epyt_flow/data/networks.py +404 -40
- epyt_flow/rest_api/base_handler.py +14 -0
- epyt_flow/rest_api/scada_data/handlers.py +42 -0
- epyt_flow/rest_api/server.py +3 -1
- epyt_flow/simulation/events/leakages.py +28 -18
- epyt_flow/simulation/parallel_simulation.py +7 -7
- epyt_flow/simulation/scada/scada_data.py +543 -12
- epyt_flow/simulation/scada/scada_data_export.py +38 -5
- epyt_flow/simulation/scenario_config.py +7 -5
- epyt_flow/simulation/scenario_simulator.py +81 -48
- epyt_flow/simulation/sensor_config.py +342 -47
- epyt_flow/topology.py +313 -11
- epyt_flow/uncertainty/model_uncertainty.py +26 -19
- epyt_flow/utils.py +1 -1
- {epyt_flow-0.2.0.dist-info → epyt_flow-0.4.0.dist-info}/METADATA +18 -6
- {epyt_flow-0.2.0.dist-info → epyt_flow-0.4.0.dist-info}/RECORD +23 -22
- {epyt_flow-0.2.0.dist-info → epyt_flow-0.4.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.2.0.dist-info → epyt_flow-0.4.0.dist-info}/WHEEL +0 -0
- {epyt_flow-0.2.0.dist-info → epyt_flow-0.4.0.dist-info}/top_level.txt +0 -0
epyt_flow/topology.py
CHANGED
|
@@ -3,9 +3,12 @@ Module provides a class for representing the topology of WDN.
|
|
|
3
3
|
"""
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
import warnings
|
|
6
|
+
from typing import Any
|
|
6
7
|
import numpy as np
|
|
7
8
|
import networkx as nx
|
|
8
9
|
from scipy.sparse import bsr_array
|
|
10
|
+
from geopandas import GeoDataFrame
|
|
11
|
+
from shapely.geometry import Point, LineString
|
|
9
12
|
|
|
10
13
|
from .serialization import serializable, JsonSerializable, NETWORK_TOPOLOGY_ID
|
|
11
14
|
|
|
@@ -14,6 +17,35 @@ UNITS_USCUSTOM = 0
|
|
|
14
17
|
UNITS_SIMETRIC = 1
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
def unitscategoryid_to_str(unit_category_id: int) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Converts a given units category ID to the corresponding description.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
unit_category_id : `int`
|
|
27
|
+
ID of the units category.
|
|
28
|
+
|
|
29
|
+
Must be one of the following constants:
|
|
30
|
+
|
|
31
|
+
- UNITS_USCUSTOM = 0
|
|
32
|
+
- UNITS_SIMETRIC = 1
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
`str`
|
|
37
|
+
Units category description.
|
|
38
|
+
"""
|
|
39
|
+
if unit_category_id is None:
|
|
40
|
+
return ""
|
|
41
|
+
elif unit_category_id == UNITS_USCUSTOM:
|
|
42
|
+
return "US CUSTOMARY"
|
|
43
|
+
elif unit_category_id == UNITS_SIMETRIC:
|
|
44
|
+
return "SI METRIC"
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(f"Unknown units category ID '{unit_category_id}'")
|
|
47
|
+
|
|
48
|
+
|
|
17
49
|
@serializable(NETWORK_TOPOLOGY_ID, ".epytflow_topology")
|
|
18
50
|
class NetworkTopology(nx.Graph, JsonSerializable):
|
|
19
51
|
"""
|
|
@@ -25,11 +57,17 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
25
57
|
Path to .inp file to which this topology belongs.
|
|
26
58
|
nodes : `list[tuple[str, dict]]`
|
|
27
59
|
List of all nodes -- i.e. node ID and node information such as type and elevation.
|
|
28
|
-
links : `list[tuple[tuple[str, str], dict]]`
|
|
60
|
+
links : `list[tuple[str, tuple[str, str], dict]]`
|
|
29
61
|
List of all links/pipes -- i.e. link ID, ID of connecting nodes, and link information
|
|
30
62
|
such as pipe diameter, length, etc.
|
|
63
|
+
pumps : `dict`
|
|
64
|
+
List of all pumps -- i.e. valve ID, and information such as
|
|
65
|
+
pump type and connecting nodes.
|
|
66
|
+
valves : `dict`
|
|
67
|
+
List of all valves -- i.e. valve ID, and information such as
|
|
68
|
+
valve type and connecting nodes.
|
|
31
69
|
units : `int`
|
|
32
|
-
Measurement units category.
|
|
70
|
+
Measurement units category -- i.e. US Customary or SI Metric.
|
|
33
71
|
|
|
34
72
|
Must be one of the following constants:
|
|
35
73
|
|
|
@@ -38,12 +76,16 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
38
76
|
"""
|
|
39
77
|
def __init__(self, f_inp: str, nodes: list[tuple[str, dict]],
|
|
40
78
|
links: list[tuple[str, tuple[str, str], dict]],
|
|
79
|
+
pumps: dict,
|
|
80
|
+
valves: dict,
|
|
41
81
|
units: int = None,
|
|
42
82
|
**kwds):
|
|
43
83
|
super().__init__(name=f_inp, **kwds)
|
|
44
84
|
|
|
45
85
|
self.__nodes = nodes
|
|
46
86
|
self.__links = links
|
|
87
|
+
self.__pumps = pumps
|
|
88
|
+
self.__valves = valves
|
|
47
89
|
self.__units = units
|
|
48
90
|
|
|
49
91
|
if units is None:
|
|
@@ -57,11 +99,83 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
57
99
|
self.add_node(node_id, info={"elevation": node_elevation, "type": node_type})
|
|
58
100
|
|
|
59
101
|
for link_id, link, link_info in links:
|
|
102
|
+
link_type = link_info["type"]
|
|
60
103
|
link_diameter = link_info["diameter"]
|
|
61
104
|
link_length = link_info["length"]
|
|
62
105
|
self.add_edge(link[0], link[1], length=link_length,
|
|
63
|
-
info={"id": link_id, "
|
|
64
|
-
"length": link_length})
|
|
106
|
+
info={"id": link_id, "type": link_type, "nodes": link,
|
|
107
|
+
"diameter": link_diameter, "length": link_length})
|
|
108
|
+
|
|
109
|
+
def convert_units(self, units: int) -> Any:
|
|
110
|
+
"""
|
|
111
|
+
Converts this instance to a :class:`epyt_flow.topology.NetworkTopology` instance
|
|
112
|
+
where everything is measured in given measurement units category
|
|
113
|
+
(US Customary or SI Metric).
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
units : `int`
|
|
118
|
+
Measurement units category.
|
|
119
|
+
|
|
120
|
+
Must be one of the following constants:
|
|
121
|
+
|
|
122
|
+
- UNITS_USCUSTOM = 0 (US Customary)
|
|
123
|
+
- UNITS_SIMETRIC = 1 (SI Metric)
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
:class:`epyt_flow.topology.NetworkTopology`
|
|
128
|
+
Network topology with the new measurements units.
|
|
129
|
+
"""
|
|
130
|
+
if self.__units is None:
|
|
131
|
+
raise ValueError("This instance does not contain any units!")
|
|
132
|
+
|
|
133
|
+
if not isinstance(units, int):
|
|
134
|
+
raise TypeError(f"'units' must be an instance of 'int' but not of '{type(units)}'")
|
|
135
|
+
if units not in [UNITS_SIMETRIC, UNITS_USCUSTOM]:
|
|
136
|
+
raise ValueError(f"Invalid units '{units}'")
|
|
137
|
+
|
|
138
|
+
if units == self.__units:
|
|
139
|
+
warnings.warn("Units already set in this NetworkTopology instance -- nothing to do!")
|
|
140
|
+
return deepcopy(self)
|
|
141
|
+
|
|
142
|
+
# Get all data and convert units
|
|
143
|
+
inch_to_millimeter = 25.4
|
|
144
|
+
feet_to_meter = 0.3048
|
|
145
|
+
|
|
146
|
+
nodes = []
|
|
147
|
+
for node_id in self.get_all_nodes():
|
|
148
|
+
node_info = self.get_node_info(node_id)
|
|
149
|
+
if units == UNITS_USCUSTOM:
|
|
150
|
+
conv_factor = 1. / feet_to_meter
|
|
151
|
+
else:
|
|
152
|
+
conv_factor = feet_to_meter
|
|
153
|
+
node_info["elevation"] *= conv_factor
|
|
154
|
+
if "diameter" in node_info:
|
|
155
|
+
node_info["diameter"] *= conv_factor
|
|
156
|
+
|
|
157
|
+
nodes.append((node_id, node_info))
|
|
158
|
+
|
|
159
|
+
links = []
|
|
160
|
+
for link_id, link_nodes in self.get_all_links():
|
|
161
|
+
link_info = self.get_link_info(link_id)
|
|
162
|
+
|
|
163
|
+
if units == UNITS_USCUSTOM:
|
|
164
|
+
conv_factor = 1. / feet_to_meter
|
|
165
|
+
else:
|
|
166
|
+
conv_factor = feet_to_meter
|
|
167
|
+
link_info["length"] *= conv_factor
|
|
168
|
+
|
|
169
|
+
if units == UNITS_USCUSTOM:
|
|
170
|
+
conv_factor = 1. / inch_to_millimeter
|
|
171
|
+
else:
|
|
172
|
+
conv_factor = inch_to_millimeter
|
|
173
|
+
link_info["diameter"] *= conv_factor
|
|
174
|
+
|
|
175
|
+
links.append((link_id, link_nodes, link_info))
|
|
176
|
+
|
|
177
|
+
return NetworkTopology(f_inp=self.name, nodes=nodes, links=links, pumps=self.pumps,
|
|
178
|
+
valves=self.valves, units=units)
|
|
65
179
|
|
|
66
180
|
def get_all_nodes(self) -> list[str]:
|
|
67
181
|
"""
|
|
@@ -85,7 +199,29 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
85
199
|
"""
|
|
86
200
|
return [(link_id, end_points) for link_id, end_points, _ in self.__links]
|
|
87
201
|
|
|
88
|
-
def
|
|
202
|
+
def get_all_pumps(self) -> list[str]:
|
|
203
|
+
"""
|
|
204
|
+
Gets the IDs of all pumps.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
`list[str]`
|
|
209
|
+
Pump IDs.
|
|
210
|
+
"""
|
|
211
|
+
return self.__pumps.keys()
|
|
212
|
+
|
|
213
|
+
def get_all_valves(self) -> list[str]:
|
|
214
|
+
"""
|
|
215
|
+
Gets the IDs of all valves.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
`list[str]`
|
|
220
|
+
Valve IDs.
|
|
221
|
+
"""
|
|
222
|
+
return self.__valves.keys()
|
|
223
|
+
|
|
224
|
+
def get_node_info(self, node_id: str) -> dict:
|
|
89
225
|
"""
|
|
90
226
|
Gets all information (e.g. elevation, type, etc.) associated with a given node.
|
|
91
227
|
|
|
@@ -105,19 +241,21 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
105
241
|
|
|
106
242
|
raise ValueError(f"Unknown node '{node_id}'")
|
|
107
243
|
|
|
108
|
-
def get_link_info(self, link_id) -> dict:
|
|
244
|
+
def get_link_info(self, link_id: str) -> dict:
|
|
109
245
|
"""
|
|
110
|
-
Gets all information (e.g. diameter, length, etc.) associated with a given link
|
|
246
|
+
Gets all information (e.g. diameter, length, etc.) associated with a given link.
|
|
247
|
+
|
|
248
|
+
Note that links can be pipes, pumps, or valves.
|
|
111
249
|
|
|
112
250
|
Parameters
|
|
113
251
|
----------
|
|
114
252
|
link_id : `str`
|
|
115
|
-
ID of the link
|
|
253
|
+
ID of the link.
|
|
116
254
|
|
|
117
255
|
Returns
|
|
118
256
|
-------
|
|
119
257
|
`dict`
|
|
120
|
-
Information associated with the given link
|
|
258
|
+
Information associated with the given link.
|
|
121
259
|
"""
|
|
122
260
|
for link_id_, link_nodes, link_info in self.__links:
|
|
123
261
|
if link_id_ == link_id:
|
|
@@ -125,6 +263,68 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
125
263
|
|
|
126
264
|
raise ValueError(f"Unknown link '{link_id}'")
|
|
127
265
|
|
|
266
|
+
def get_pump_info(self, pump_id: str) -> dict:
|
|
267
|
+
"""
|
|
268
|
+
Gets all information associated with a given pump.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
pump_id : `str`
|
|
273
|
+
ID of the pump.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
`dict`
|
|
278
|
+
Pump information.
|
|
279
|
+
"""
|
|
280
|
+
if pump_id in self.__pumps:
|
|
281
|
+
return self.__pumps[pump_id]
|
|
282
|
+
else:
|
|
283
|
+
raise ValueError(f"Unknown pump: '{pump_id}'")
|
|
284
|
+
|
|
285
|
+
def get_valve_info(self, valve_id: str) -> dict:
|
|
286
|
+
"""
|
|
287
|
+
Gets all information associated with a given valve.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
valve_id : `str`
|
|
292
|
+
ID of the valve.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
`dict`
|
|
297
|
+
Valve information.
|
|
298
|
+
"""
|
|
299
|
+
if valve_id in self.__valves:
|
|
300
|
+
return self.__valves[valve_id]
|
|
301
|
+
else:
|
|
302
|
+
raise ValueError(f"Unknown valve: '{valve_id}'")
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def pumps(self) -> dict:
|
|
306
|
+
"""
|
|
307
|
+
Gets all pumps -- i.e. ID and associated information such as the pump type.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
`dict`
|
|
312
|
+
All pumps and their associated information.
|
|
313
|
+
"""
|
|
314
|
+
return deepcopy(self.__pumps)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def valves(self) -> dict:
|
|
318
|
+
"""
|
|
319
|
+
Gets all valves -- i.e. ID and associated information such as the valve type.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
`dict`
|
|
324
|
+
All valves and their associated information.
|
|
325
|
+
"""
|
|
326
|
+
return deepcopy(self.__valves)
|
|
327
|
+
|
|
128
328
|
@property
|
|
129
329
|
def units(self) -> int:
|
|
130
330
|
"""
|
|
@@ -151,18 +351,120 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
151
351
|
self.get_all_nodes() == other.get_all_nodes() \
|
|
152
352
|
and all(link_a[0] == link_b[0] and all(link_a[1] == link_b[1])
|
|
153
353
|
for link_a, link_b in zip(self.get_all_links(), other.get_all_links())) \
|
|
154
|
-
and self.__units == other.units
|
|
354
|
+
and self.__units == other.units \
|
|
355
|
+
and self.__pumps == other.pumps \
|
|
356
|
+
and self.__valves == other.valves
|
|
155
357
|
|
|
156
358
|
def __str__(self) -> str:
|
|
157
359
|
return f"f_inp: {self.name} nodes: {self.__nodes} links: {self.__links} " +\
|
|
158
|
-
f"
|
|
360
|
+
f"pumps: {self.__pumps} valves: {self.__valves} " +\
|
|
361
|
+
f"units: {unitscategoryid_to_str(self.__units)}"
|
|
159
362
|
|
|
160
363
|
def get_attributes(self) -> dict:
|
|
161
364
|
return super().get_attributes() | {"f_inp": self.name,
|
|
162
365
|
"nodes": self.__nodes,
|
|
163
366
|
"links": self.__links,
|
|
367
|
+
"pumps": self.__pumps,
|
|
368
|
+
"valves": self.__valves,
|
|
164
369
|
"units": self.__units}
|
|
165
370
|
|
|
371
|
+
def to_gis(self, coord_reference_system: str = None, pumps_as_points: bool = False,
|
|
372
|
+
valves_as_points: bool = False) -> dict:
|
|
373
|
+
"""
|
|
374
|
+
Gets the network topology as a dictionary of `geopandas.GeoDataFrames` instances --
|
|
375
|
+
i.e. each quantity (nodes, links/pipes, valves, etc.) is represented by a
|
|
376
|
+
`geopandas.GeoDataFrames` instance.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
coord_reference_system : `str`, optional
|
|
381
|
+
Coordinate reference system.
|
|
382
|
+
|
|
383
|
+
The default is None.
|
|
384
|
+
pumps_as_points : `bool`, optional
|
|
385
|
+
If True, pumps are represented by points, otherwise by lines.
|
|
386
|
+
|
|
387
|
+
The default is False.
|
|
388
|
+
|
|
389
|
+
valves_as_points : `bool`, optional
|
|
390
|
+
If True, valves are represented by points, otherwise by lines.
|
|
391
|
+
|
|
392
|
+
The default is False.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
`dict`
|
|
397
|
+
Network topology as a dictionary of `geopandas.GeoDataFrames` instances.
|
|
398
|
+
If a quantity does not exist, the data frame will be None.
|
|
399
|
+
"""
|
|
400
|
+
gis = {"nodes": None, "links": None,
|
|
401
|
+
"tanks": None, "reservoirs": None,
|
|
402
|
+
"valves": None, "pumps": None}
|
|
403
|
+
|
|
404
|
+
# Nodes
|
|
405
|
+
node_data = {"id": [], "type": [], "elevation": [], "geometry": []}
|
|
406
|
+
tank_data = {"id": [], "elevation": [], "diameter": [], "geometry": []}
|
|
407
|
+
reservoir_data = {"id": [], "elevation": [], "geometry": []}
|
|
408
|
+
for node_id in self.get_all_nodes():
|
|
409
|
+
node_info = self.get_node_info(node_id)
|
|
410
|
+
|
|
411
|
+
node_data["id"].append(node_id)
|
|
412
|
+
node_data["type"].append(node_info["type"])
|
|
413
|
+
node_data["elevation"].append(node_info["elevation"])
|
|
414
|
+
node_data["geometry"].append(Point(node_info["coord"]))
|
|
415
|
+
|
|
416
|
+
if node_info["type"] == "TANK":
|
|
417
|
+
tank_data["id"].append(node_id)
|
|
418
|
+
tank_data["elevation"].append(node_info["elevation"])
|
|
419
|
+
tank_data["diameter"].append(node_info["diameter"])
|
|
420
|
+
tank_data["geometry"].append(Point(node_info["coord"]))
|
|
421
|
+
elif node_info["type"] == "RESERVOIR":
|
|
422
|
+
reservoir_data["id"].append(node_id)
|
|
423
|
+
reservoir_data["elevation"].append(node_info["elevation"])
|
|
424
|
+
reservoir_data["geometry"].append(Point(node_info["coord"]))
|
|
425
|
+
|
|
426
|
+
gis["nodes"] = GeoDataFrame(node_data, crs=coord_reference_system)
|
|
427
|
+
gis["tanks"] = GeoDataFrame(tank_data, crs=coord_reference_system)
|
|
428
|
+
gis["reservoirs"] = GeoDataFrame(reservoir_data, crs=coord_reference_system)
|
|
429
|
+
|
|
430
|
+
# Links
|
|
431
|
+
pipe_data = {"id": [], "type": [], "end_point_a": [], "end_point_b": [],
|
|
432
|
+
"length": [], "diameter": [], "geometry": []}
|
|
433
|
+
valve_data = {"id": [], "type": [], "geometry": []}
|
|
434
|
+
pump_data = {"id": [], "type": [], "geometry": []}
|
|
435
|
+
for link_id, link_nodes in self.get_all_links():
|
|
436
|
+
link_info = self.get_link_info(link_id)
|
|
437
|
+
end_points_coord = [self.get_node_info(n)["coord"] for n in link_nodes]
|
|
438
|
+
|
|
439
|
+
if link_info["type"] == "PIPE":
|
|
440
|
+
pipe_data["id"].append(link_id)
|
|
441
|
+
pipe_data["type"].append(link_info["type"])
|
|
442
|
+
pipe_data["end_point_a"].append(link_nodes[0])
|
|
443
|
+
pipe_data["end_point_b"].append(link_nodes[1])
|
|
444
|
+
pipe_data["length"].append(link_info["length"])
|
|
445
|
+
pipe_data["diameter"].append(link_info["diameter"])
|
|
446
|
+
pipe_data["geometry"].append(LineString(end_points_coord))
|
|
447
|
+
elif link_info["type"] == "PUMP":
|
|
448
|
+
pump_data["id"].append(link_id)
|
|
449
|
+
pump_data["type"].append(self.get_pump_info(link_id)["type"])
|
|
450
|
+
if pumps_as_points is True:
|
|
451
|
+
pump_data["geometry"].append(Point(end_points_coord[0]))
|
|
452
|
+
else:
|
|
453
|
+
pump_data["geometry"].append(LineString(end_points_coord))
|
|
454
|
+
else: # Valve
|
|
455
|
+
valve_data["id"].append(link_id)
|
|
456
|
+
valve_data["type"].append(self.get_valve_info[link_id]["type"])
|
|
457
|
+
if valves_as_points is True:
|
|
458
|
+
valve_data["geometry"].append(Point(end_points_coord[0]))
|
|
459
|
+
else:
|
|
460
|
+
valve_data["geometry"].append(LineString(end_points_coord))
|
|
461
|
+
|
|
462
|
+
gis["pipes"] = GeoDataFrame(pipe_data, crs=coord_reference_system)
|
|
463
|
+
gis["valves"] = GeoDataFrame(valve_data, crs=coord_reference_system)
|
|
464
|
+
gis["pumps"] = GeoDataFrame(pump_data, crs=coord_reference_system)
|
|
465
|
+
|
|
466
|
+
return gis
|
|
467
|
+
|
|
166
468
|
def get_adj_matrix(self) -> bsr_array:
|
|
167
469
|
"""
|
|
168
470
|
Gets the adjacency matrix of this graph.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Module provides a class for implementing model uncertainty.
|
|
3
3
|
"""
|
|
4
4
|
from copy import deepcopy
|
|
5
|
+
import warnings
|
|
5
6
|
import epyt
|
|
6
7
|
import numpy as np
|
|
7
8
|
|
|
@@ -29,7 +30,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
29
30
|
Uncertainty of pipe diameters. None, in the case of no uncertainty.
|
|
30
31
|
|
|
31
32
|
The default is None.
|
|
32
|
-
|
|
33
|
+
base_demand_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
33
34
|
Uncertainty of base demands. None, in the case of no uncertainty.
|
|
34
35
|
|
|
35
36
|
The default is None.
|
|
@@ -53,11 +54,17 @@ class ModelUncertainty(JsonSerializable):
|
|
|
53
54
|
def __init__(self, pipe_length_uncertainty: Uncertainty = None,
|
|
54
55
|
pipe_roughness_uncertainty: Uncertainty = None,
|
|
55
56
|
pipe_diameter_uncertainty: Uncertainty = None,
|
|
56
|
-
|
|
57
|
+
base_demand_uncertainty: Uncertainty = None,
|
|
57
58
|
demand_pattern_uncertainty: Uncertainty = None,
|
|
58
59
|
elevation_uncertainty: Uncertainty = None,
|
|
59
60
|
constants_uncertainty: Uncertainty = None,
|
|
60
|
-
parameters_uncertainty: Uncertainty = None,
|
|
61
|
+
parameters_uncertainty: Uncertainty = None,
|
|
62
|
+
demand_base_uncertainty: Uncertainty = None, **kwds):
|
|
63
|
+
if demand_base_uncertainty is not None:
|
|
64
|
+
warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
|
|
65
|
+
" -- support of such old files will be removed in the next release!",
|
|
66
|
+
DeprecationWarning)
|
|
67
|
+
|
|
61
68
|
if pipe_length_uncertainty is not None:
|
|
62
69
|
if not isinstance(pipe_length_uncertainty, Uncertainty):
|
|
63
70
|
raise TypeError("'pipe_length_uncertainty' must be an instance of " +
|
|
@@ -73,11 +80,11 @@ class ModelUncertainty(JsonSerializable):
|
|
|
73
80
|
raise TypeError("'pipe_diameter_uncertainty' must be an instance of " +
|
|
74
81
|
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
75
82
|
f"'{type(pipe_diameter_uncertainty)}'")
|
|
76
|
-
if
|
|
77
|
-
if not isinstance(
|
|
78
|
-
raise TypeError("'
|
|
83
|
+
if base_demand_uncertainty is not None:
|
|
84
|
+
if not isinstance(base_demand_uncertainty, Uncertainty):
|
|
85
|
+
raise TypeError("'base_demand_uncertainty' must be an instance of " +
|
|
79
86
|
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
80
|
-
f"'{type(
|
|
87
|
+
f"'{type(base_demand_uncertainty)}'")
|
|
81
88
|
if demand_pattern_uncertainty is not None:
|
|
82
89
|
if not isinstance(demand_pattern_uncertainty, Uncertainty):
|
|
83
90
|
raise TypeError("'demand_pattern_uncertainty' must be an instance of " +
|
|
@@ -102,7 +109,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
102
109
|
self.__pipe_length = pipe_length_uncertainty
|
|
103
110
|
self.__pipe_roughness = pipe_roughness_uncertainty
|
|
104
111
|
self.__pipe_diameter = pipe_diameter_uncertainty
|
|
105
|
-
self.
|
|
112
|
+
self.__base_demand = base_demand_uncertainty
|
|
106
113
|
self.__demand_pattern = demand_pattern_uncertainty
|
|
107
114
|
self.__elevation = elevation_uncertainty
|
|
108
115
|
self.__constants = constants_uncertainty
|
|
@@ -147,16 +154,16 @@ class ModelUncertainty(JsonSerializable):
|
|
|
147
154
|
return deepcopy(self.__pipe_diameter)
|
|
148
155
|
|
|
149
156
|
@property
|
|
150
|
-
def
|
|
157
|
+
def base_demand(self) -> Uncertainty:
|
|
151
158
|
"""
|
|
152
|
-
Gets the demand
|
|
159
|
+
Gets the base demand uncertainty.
|
|
153
160
|
|
|
154
161
|
Returns
|
|
155
162
|
-------
|
|
156
163
|
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
157
164
|
Demand base uncertainty.
|
|
158
165
|
"""
|
|
159
|
-
return deepcopy(self.
|
|
166
|
+
return deepcopy(self.__base_demand)
|
|
160
167
|
|
|
161
168
|
@property
|
|
162
169
|
def demand_pattern(self) -> Uncertainty:
|
|
@@ -210,7 +217,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
210
217
|
return super().get_attributes() | {"pipe_length_uncertainty": self.__pipe_length,
|
|
211
218
|
"pipe_roughness_uncertainty": self.__pipe_roughness,
|
|
212
219
|
"pipe_diameter_uncertainty": self.__pipe_diameter,
|
|
213
|
-
"
|
|
220
|
+
"base_demand_uncertainty": self.__base_demand,
|
|
214
221
|
"demand_pattern_uncertainty": self.__demand_pattern,
|
|
215
222
|
"elevation_uncertainty": self.__elevation,
|
|
216
223
|
"constants_uncertainty": self.__constants,
|
|
@@ -224,14 +231,14 @@ class ModelUncertainty(JsonSerializable):
|
|
|
224
231
|
return self.__pipe_length == other.pipe_length \
|
|
225
232
|
and self.__pipe_roughness == other.pipe_roughness \
|
|
226
233
|
and self.__pipe_diameter == other.pipe_diameter \
|
|
227
|
-
and self.
|
|
234
|
+
and self.__base_demand == other.base_demand \
|
|
228
235
|
and self.__demand_pattern == other.demand_pattern \
|
|
229
236
|
and self.__elevation == other.elevation \
|
|
230
237
|
and self.__parameters == other.parameters and self.__constants == other.constants
|
|
231
238
|
|
|
232
239
|
def __str__(self) -> str:
|
|
233
240
|
return f"pipe_length: {self.__pipe_length} pipe_roughness: {self.__pipe_roughness} " + \
|
|
234
|
-
f"pipe_diameter: {self.__pipe_diameter} demand_base: {self.
|
|
241
|
+
f"pipe_diameter: {self.__pipe_diameter} demand_base: {self.__base_demand} " + \
|
|
235
242
|
f"demand_pattern: {self.__demand_pattern} elevation: {self.__elevation} " + \
|
|
236
243
|
f"constants: {self.__constants} parameters: {self.__parameters}"
|
|
237
244
|
|
|
@@ -259,14 +266,14 @@ class ModelUncertainty(JsonSerializable):
|
|
|
259
266
|
coeffs = self.__pipe_roughness.apply_batch(coeffs)
|
|
260
267
|
epanet_api.setLinkRoughnessCoeff(coeffs)
|
|
261
268
|
|
|
262
|
-
if self.
|
|
269
|
+
if self.__base_demand is not None:
|
|
263
270
|
all_nodes_idx = epanet_api.getNodeIndex()
|
|
264
271
|
for node_idx in all_nodes_idx:
|
|
265
272
|
n_demand_categories = epanet_api.getNodeDemandCategoriesNumber(node_idx)
|
|
266
|
-
for demand_category in range(
|
|
267
|
-
base_demand = epanet_api.getNodeBaseDemands(node_idx)[demand_category]
|
|
268
|
-
base_demand = self.
|
|
269
|
-
epanet_api.setNodeBaseDemands(node_idx, demand_category, base_demand)
|
|
273
|
+
for demand_category in range(n_demand_categories):
|
|
274
|
+
base_demand = epanet_api.getNodeBaseDemands(node_idx)[demand_category + 1]
|
|
275
|
+
base_demand = self.__base_demand.apply(base_demand)
|
|
276
|
+
epanet_api.setNodeBaseDemands(node_idx, demand_category + 1, base_demand)
|
|
270
277
|
|
|
271
278
|
if self.__demand_pattern is not None:
|
|
272
279
|
demand_patterns_idx = epanet_api.getNodeDemandPatternIndex()
|
epyt_flow/utils.py
CHANGED
|
@@ -214,8 +214,8 @@ def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
|
|
|
214
214
|
y_pred - confidence_interval[0],
|
|
215
215
|
y_pred + confidence_interval[1],
|
|
216
216
|
alpha=0.5)
|
|
217
|
-
ax.plot(y_pred, ".-", label="Prediction")
|
|
218
217
|
ax.plot(y, ".-", label="Ground truth")
|
|
218
|
+
ax.plot(y_pred, ".-", label="Prediction")
|
|
219
219
|
ax.legend()
|
|
220
220
|
|
|
221
221
|
if show is True and fig is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: epyt-flow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: EPyT-Flow -- EPANET Python Toolkit - Flow
|
|
5
5
|
Author-email: André Artelt <aartelt@techfak.uni-bielefeld.de>, "Marios S. Kyriakou" <kiriakou.marios@ucy.ac.cy>, "Stelios G. Vrachimis" <vrachimis.stelios@ucy.ac.cy>
|
|
6
6
|
License: MIT License
|
|
@@ -30,10 +30,12 @@ Requires-Dist: tqdm >=4.66.2
|
|
|
30
30
|
Requires-Dist: openpyxl >=3.1.2
|
|
31
31
|
Requires-Dist: falcon >=3.1.3
|
|
32
32
|
Requires-Dist: multiprocess >=0.70.16
|
|
33
|
+
Requires-Dist: geopandas >=0.14.4
|
|
33
34
|
Requires-Dist: psutil
|
|
34
35
|
|
|
35
|
-
[](https://opensource.org/licenses/MIT)
|
|
36
36
|
[](https://pypi.org/project/epyt-flow/)
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+

|
|
37
39
|
[](https://github.com/WaterFutures/EPyT-Flow/actions/workflows/build_tests.yml)
|
|
38
40
|
[](https://epyt-flow.readthedocs.io/en/stable/?badge=stable)
|
|
39
41
|
[](https://pepy.tech/project/epyt-flow)
|
|
@@ -41,6 +43,8 @@ Requires-Dist: psutil
|
|
|
41
43
|
|
|
42
44
|
# EPyT-Flow -- EPANET Python Toolkit - Flow
|
|
43
45
|
|
|
46
|
+
<img src="https://github.com/WaterFutures/EPyT-Flow/blob/main/docs/_static/net1_plot.png?raw=true" align="right" height="230px"/>
|
|
47
|
+
|
|
44
48
|
EPyT-Flow is a Python package building on top of [EPyT](https://github.com/OpenWaterAnalytics/EPyT)
|
|
45
49
|
for providing easy access to water distribution network simulations.
|
|
46
50
|
It aims to provide a high-level interface for the easy generation of hydraulic and water quality scenario data.
|
|
@@ -50,8 +54,6 @@ and [EPANET-MSX](https://github.com/USEPA/EPANETMSX/).
|
|
|
50
54
|
EPyT-Flow provides easy access to popular benchmark data sets for event detection and localization.
|
|
51
55
|
Furthermore, it also provides an environment for developing and testing control algorithms.
|
|
52
56
|
|
|
53
|
-

|
|
54
|
-
|
|
55
57
|
|
|
56
58
|
## Unique Features
|
|
57
59
|
|
|
@@ -74,8 +76,18 @@ Unique features of EPyT-Flow that make it superior to other (Python) toolboxes a
|
|
|
74
76
|
EPyT-Flow supports Python 3.9 - 3.12
|
|
75
77
|
|
|
76
78
|
Note that [EPANET and EPANET-MSX sources](epyt_flow/EPANET/) are compiled and overwrite the binaries
|
|
77
|
-
shipped by EPyT IF EPyT-Flow is installed on a
|
|
78
|
-
a better performance of the simulations but also avoid any
|
|
79
|
+
shipped by EPyT **IF** EPyT-Flow is installed on a Unix system and the *gcc* compiler is available.
|
|
80
|
+
By this, we not only aim to achieve a better performance of the simulations but also avoid any
|
|
81
|
+
compatibility issues of pre-compiled binaries.
|
|
82
|
+
|
|
83
|
+
#### Prerequisites for macOS users
|
|
84
|
+
The "true" *gcc* compiler (version 12) is needed which is not the
|
|
85
|
+
*clang* compiler that is shipped with Xcode and is linked to gcc!
|
|
86
|
+
|
|
87
|
+
The correct version of the "true" *gcc* can be installed via [brew](https://brew.sh/):
|
|
88
|
+
```
|
|
89
|
+
brew install gcc@12
|
|
90
|
+
```
|
|
79
91
|
|
|
80
92
|
### PyPI
|
|
81
93
|
|