epyt-flow 0.1.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/EPANET/SRC_engines/AUTHORS +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
- epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
- epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
- epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
- epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
- epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
- epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
- epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
- epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
- epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
- epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
- epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
- epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
- epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
- epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
- epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
- epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
- epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
- epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
- epyt_flow/EPANET/compile.sh +4 -0
- epyt_flow/VERSION +1 -0
- epyt_flow/__init__.py +24 -0
- epyt_flow/data/__init__.py +0 -0
- epyt_flow/data/benchmarks/__init__.py +11 -0
- epyt_flow/data/benchmarks/batadal.py +257 -0
- epyt_flow/data/benchmarks/batadal_data.py +28 -0
- epyt_flow/data/benchmarks/battledim.py +473 -0
- epyt_flow/data/benchmarks/battledim_data.py +51 -0
- epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
- epyt_flow/data/benchmarks/leakdb.py +592 -0
- epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
- epyt_flow/data/benchmarks/water_usage.py +123 -0
- epyt_flow/data/networks.py +650 -0
- epyt_flow/gym/__init__.py +4 -0
- epyt_flow/gym/control_gyms.py +47 -0
- epyt_flow/gym/scenario_control_env.py +101 -0
- epyt_flow/metrics.py +404 -0
- epyt_flow/models/__init__.py +2 -0
- epyt_flow/models/event_detector.py +31 -0
- epyt_flow/models/sensor_interpolation_detector.py +118 -0
- epyt_flow/rest_api/__init__.py +4 -0
- epyt_flow/rest_api/base_handler.py +70 -0
- epyt_flow/rest_api/res_manager.py +95 -0
- epyt_flow/rest_api/scada_data_handler.py +476 -0
- epyt_flow/rest_api/scenario_handler.py +352 -0
- epyt_flow/rest_api/server.py +106 -0
- epyt_flow/serialization.py +438 -0
- epyt_flow/simulation/__init__.py +5 -0
- epyt_flow/simulation/events/__init__.py +6 -0
- epyt_flow/simulation/events/actuator_events.py +259 -0
- epyt_flow/simulation/events/event.py +81 -0
- epyt_flow/simulation/events/leakages.py +404 -0
- epyt_flow/simulation/events/sensor_faults.py +267 -0
- epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
- epyt_flow/simulation/events/sensor_reading_event.py +170 -0
- epyt_flow/simulation/events/system_event.py +88 -0
- epyt_flow/simulation/parallel_simulation.py +147 -0
- epyt_flow/simulation/scada/__init__.py +3 -0
- epyt_flow/simulation/scada/advanced_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +1589 -0
- epyt_flow/simulation/scada/scada_data_export.py +255 -0
- epyt_flow/simulation/scenario_config.py +608 -0
- epyt_flow/simulation/scenario_simulator.py +1897 -0
- epyt_flow/simulation/scenario_visualizer.py +61 -0
- epyt_flow/simulation/sensor_config.py +1289 -0
- epyt_flow/topology.py +290 -0
- epyt_flow/uncertainty/__init__.py +3 -0
- epyt_flow/uncertainty/model_uncertainty.py +302 -0
- epyt_flow/uncertainty/sensor_noise.py +73 -0
- epyt_flow/uncertainty/uncertainties.py +555 -0
- epyt_flow/uncertainty/utils.py +206 -0
- epyt_flow/utils.py +306 -0
- epyt_flow-0.1.0.dist-info/LICENSE +21 -0
- epyt_flow-0.1.0.dist-info/METADATA +139 -0
- epyt_flow-0.1.0.dist-info/RECORD +131 -0
- epyt_flow-0.1.0.dist-info/WHEEL +5 -0
- epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
epyt_flow/topology.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a class for representing the topology of WDN.
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from scipy.sparse import bsr_array
|
|
7
|
+
|
|
8
|
+
from .serialization import serializable, JsonSerializable, NETWORK_TOPOLOGY_ID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@serializable(NETWORK_TOPOLOGY_ID, ".epytflow_topology")
|
|
12
|
+
class NetworkTopology(nx.Graph, JsonSerializable):
|
|
13
|
+
"""
|
|
14
|
+
Class representing the topology of a WDN.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
f_inp : `str`
|
|
19
|
+
Path to .inp file to which this topology belongs.
|
|
20
|
+
nodes : `list[tuple[str, dict]]`
|
|
21
|
+
List of all nodes -- i.e. node ID and node information such as type and elevation.
|
|
22
|
+
links : `list[tuple[tuple[str, str], dict]]`
|
|
23
|
+
List of all links/pipes -- i.e. link ID, ID of connecting nodes, and link information
|
|
24
|
+
such as pipe diameter, length, etc.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, f_inp: str, nodes: list[tuple[str, dict]],
|
|
27
|
+
links: list[tuple[str, tuple[str, str], dict]], **kwds):
|
|
28
|
+
super().__init__(name=f_inp, **kwds)
|
|
29
|
+
|
|
30
|
+
self.__nodes = nodes
|
|
31
|
+
self.__links = links
|
|
32
|
+
|
|
33
|
+
for node_id, node_info in nodes:
|
|
34
|
+
node_elevation = node_info["elevation"]
|
|
35
|
+
node_type = node_info["type"]
|
|
36
|
+
self.add_node(node_id, info={"elevation": node_elevation, "type": node_type})
|
|
37
|
+
|
|
38
|
+
for link_id, link, link_info in links:
|
|
39
|
+
link_diameter = link_info["diameter"]
|
|
40
|
+
link_length = link_info["length"]
|
|
41
|
+
self.add_edge(link[0], link[1], length=link_length,
|
|
42
|
+
info={"id": link_id, "nodes": link, "diameter": link_diameter,
|
|
43
|
+
"length": link_length})
|
|
44
|
+
|
|
45
|
+
def get_all_nodes(self) -> list[str]:
|
|
46
|
+
"""
|
|
47
|
+
Gets a list of all nodes.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
`list[str]`
|
|
52
|
+
List of all nodes ID.
|
|
53
|
+
"""
|
|
54
|
+
return [node_id for node_id, _ in self.__nodes]
|
|
55
|
+
|
|
56
|
+
def get_all_links(self) -> list[tuple[str, tuple[str, str]]]:
|
|
57
|
+
"""
|
|
58
|
+
Gets a list of all links/pipes (incl. their end points).
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
`list[tuple[str, tuple[str, str]]]`
|
|
63
|
+
List of links -- (link ID, (left node ID, right node ID)).
|
|
64
|
+
"""
|
|
65
|
+
return [(link_id, end_points) for link_id, end_points, _ in self.__links]
|
|
66
|
+
|
|
67
|
+
def get_node_info(self, node_id) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Gets all information (e.g. elevation, type, etc.) associated with a given node.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
node_id : `str`
|
|
74
|
+
ID of the node.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
`dict`
|
|
79
|
+
Information associated with the given node.
|
|
80
|
+
"""
|
|
81
|
+
for node_id_, node_info in self.__nodes:
|
|
82
|
+
if node_id_ == node_id:
|
|
83
|
+
return node_info
|
|
84
|
+
|
|
85
|
+
raise ValueError(f"Unknown node '{node_id}'")
|
|
86
|
+
|
|
87
|
+
def get_link_info(self, link_id) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
Gets all information (e.g. diameter, length, etc.) associated with a given link/pipe.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
link_id : `str`
|
|
94
|
+
ID of the link/pipe.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
`dict`
|
|
99
|
+
Information associated with the given link/pipe.
|
|
100
|
+
"""
|
|
101
|
+
for link_id_, link_nodes, link_info in self.__links:
|
|
102
|
+
if link_id_ == link_id:
|
|
103
|
+
return {"nodes": link_nodes} | link_info
|
|
104
|
+
|
|
105
|
+
raise ValueError(f"Unknown link '{link_id}'")
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other) -> bool:
|
|
108
|
+
if not isinstance(other, NetworkTopology):
|
|
109
|
+
raise TypeError("Can not compare 'NetworkTopology' instance to " +
|
|
110
|
+
f"'{type(other)}' instance")
|
|
111
|
+
|
|
112
|
+
return super().__eq__(other) and \
|
|
113
|
+
self.get_all_nodes() == other.get_all_nodes() and \
|
|
114
|
+
all(link_a[0] == link_b[0] and all(link_a[1] == link_b[1])
|
|
115
|
+
for link_a, link_b in zip(self.get_all_links(), other.get_all_links()))
|
|
116
|
+
|
|
117
|
+
def __str__(self) -> str:
|
|
118
|
+
return f"f_inp: {self.name} nodes: {self.__nodes} links: {self.__links}"
|
|
119
|
+
|
|
120
|
+
def get_attributes(self) -> dict:
|
|
121
|
+
return super().get_attributes() | {"f_inp": self.name,
|
|
122
|
+
"nodes": self.__nodes,
|
|
123
|
+
"links": self.__links}
|
|
124
|
+
|
|
125
|
+
def get_adj_matrix(self) -> bsr_array:
|
|
126
|
+
"""
|
|
127
|
+
Gets the adjacency matrix of this graph.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
`scipy.bsr_array`
|
|
132
|
+
Adjacency matrix as a sparse array.
|
|
133
|
+
"""
|
|
134
|
+
nodes_id = [node_id for node_id, _ in self.__nodes]
|
|
135
|
+
n_nodes = len(self.__nodes)
|
|
136
|
+
|
|
137
|
+
row = []
|
|
138
|
+
col = []
|
|
139
|
+
for _, link_end_points, _ in self.__links:
|
|
140
|
+
a = nodes_id.index(link_end_points[0])
|
|
141
|
+
b = nodes_id.index(link_end_points[1])
|
|
142
|
+
|
|
143
|
+
row.append(a)
|
|
144
|
+
col.append(b)
|
|
145
|
+
|
|
146
|
+
row.append(b)
|
|
147
|
+
col.append(a)
|
|
148
|
+
|
|
149
|
+
for i in range(n_nodes):
|
|
150
|
+
row.append(i)
|
|
151
|
+
col.append(i)
|
|
152
|
+
|
|
153
|
+
return bsr_array((np.ones(len(row)), (row, col)), shape=(n_nodes, n_nodes))
|
|
154
|
+
|
|
155
|
+
def get_neighbors(self, node_id: str) -> list[str]:
|
|
156
|
+
"""
|
|
157
|
+
Gets all neighboring nodes of a given node.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
node_id : `str`
|
|
162
|
+
ID of the node.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
`list[str]`
|
|
167
|
+
IDs of neighboring nodes.
|
|
168
|
+
"""
|
|
169
|
+
if node_id not in self.get_all_nodes():
|
|
170
|
+
raise ValueError(f"Unknown node '{node_id}'")
|
|
171
|
+
|
|
172
|
+
return list(self.neighbors(node_id))
|
|
173
|
+
|
|
174
|
+
def get_adjacent_links(self, node_id: str) -> list[tuple[str, tuple[str, str]]]:
|
|
175
|
+
"""
|
|
176
|
+
Gets all adjacent links/pipes of a given node.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
node_id : `str`
|
|
181
|
+
ID of the node.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
`list[tuple[str, tuple[str, str]]]`
|
|
186
|
+
Adjacent links -- i.e. (link ID, IDs of node end points).
|
|
187
|
+
"""
|
|
188
|
+
if node_id not in self.get_all_nodes():
|
|
189
|
+
raise ValueError(f"Unknown node '{node_id}'")
|
|
190
|
+
|
|
191
|
+
links = []
|
|
192
|
+
|
|
193
|
+
for link_id, nodes_id, _ in self.__links:
|
|
194
|
+
if node_id in nodes_id:
|
|
195
|
+
links.append((link_id, nodes_id))
|
|
196
|
+
|
|
197
|
+
return links
|
|
198
|
+
|
|
199
|
+
def get_shortest_path(self, start_node_id: str, end_node_id: str,
|
|
200
|
+
use_pipe_length_as_weight: bool = True) -> list[str]:
|
|
201
|
+
"""
|
|
202
|
+
Computes the shortest path between two nodes in this graph.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
start_node_id : `str`
|
|
207
|
+
ID of start node.
|
|
208
|
+
end_node_id : `str`
|
|
209
|
+
ID of end node.
|
|
210
|
+
use_pipe_length_as_weight : `bool`, optional
|
|
211
|
+
If True, pipe lengths are used for the edge weights -- otherwise,
|
|
212
|
+
each edge weight is set to one.
|
|
213
|
+
|
|
214
|
+
The default is True.
|
|
215
|
+
"""
|
|
216
|
+
if start_node_id not in self.get_all_nodes():
|
|
217
|
+
raise ValueError(f"Unknown node '{start_node_id}'")
|
|
218
|
+
if end_node_id not in self.get_all_nodes():
|
|
219
|
+
raise ValueError(f"Unknown node '{end_node_id}'")
|
|
220
|
+
|
|
221
|
+
weight = "length" if use_pipe_length_as_weight is True else None
|
|
222
|
+
return nx.shortest_path(self, source=start_node_id, target=end_node_id, weight=weight)
|
|
223
|
+
|
|
224
|
+
def get_all_pairs_shortest_path(self, use_pipe_length_as_weight: bool = True) -> dict:
|
|
225
|
+
"""
|
|
226
|
+
Computes the shortest path between all pairs of nodes in this graph.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
use_pipe_length_as_weight : `bool`, optional
|
|
231
|
+
If True, pipe lengths are used for the edge weights -- otherwise,
|
|
232
|
+
each edge weight is set to one.
|
|
233
|
+
|
|
234
|
+
The default is True.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
`dict`
|
|
239
|
+
Shortest paths between all pairs of nodes as nested dictionaries --
|
|
240
|
+
first key is the start node, second key is the end node.
|
|
241
|
+
"""
|
|
242
|
+
weight = "length" if use_pipe_length_as_weight is True else None
|
|
243
|
+
return nx.shortest_path(self, weight=weight)
|
|
244
|
+
|
|
245
|
+
def get_shortest_path_length(self, start_node_id: str, end_node_id: str,
|
|
246
|
+
use_pipe_length_as_weight: bool = True) -> list[str]:
|
|
247
|
+
"""
|
|
248
|
+
Computes the shortest path length between two nodes in this graph.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
start_node_id : `str`
|
|
253
|
+
ID of start node.
|
|
254
|
+
end_node_id : `str`
|
|
255
|
+
ID of end node.
|
|
256
|
+
use_pipe_length_as_weight : `bool`, optional
|
|
257
|
+
If True, pipe lengths are used for the edge weights -- otherwise,
|
|
258
|
+
each edge weight is set to one.
|
|
259
|
+
|
|
260
|
+
The default is True.
|
|
261
|
+
"""
|
|
262
|
+
if start_node_id not in self.get_all_nodes():
|
|
263
|
+
raise ValueError(f"Unknown node '{start_node_id}'")
|
|
264
|
+
if end_node_id not in self.get_all_nodes():
|
|
265
|
+
raise ValueError(f"Unknown node '{end_node_id}'")
|
|
266
|
+
|
|
267
|
+
weight = "length" if use_pipe_length_as_weight is True else None
|
|
268
|
+
return nx.shortest_path_length(self, source=start_node_id, target=end_node_id,
|
|
269
|
+
weight=weight)
|
|
270
|
+
|
|
271
|
+
def get_all_pairs_shortest_path_length(self, use_pipe_length_as_weight: bool = True) -> dict:
|
|
272
|
+
"""
|
|
273
|
+
Computes the shortest path length between all pairs of nodes in this graph.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
use_pipe_length_as_weight : `bool`, optional
|
|
278
|
+
If True, pipe lengths are used for the edge weights -- otherwise,
|
|
279
|
+
each edge weight is set to one.
|
|
280
|
+
|
|
281
|
+
The default is True.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
`dict`
|
|
286
|
+
Shortest paths between all pairs of nodes as nested dictionaries --
|
|
287
|
+
first key is the start node, second key is the end node.
|
|
288
|
+
"""
|
|
289
|
+
weight = "length" if use_pipe_length_as_weight is True else None
|
|
290
|
+
return dict(nx.shortest_path_length(self, weight=weight))
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a class for implementing model uncertainty.
|
|
3
|
+
"""
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
import epyt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..serialization import serializable, JsonSerializable, MODEL_UNCERTAINTY_ID
|
|
9
|
+
from .uncertainties import Uncertainty
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@serializable(MODEL_UNCERTAINTY_ID, ".epytflow_uncertainty_model_uncertainty")
|
|
13
|
+
class ModelUncertainty(JsonSerializable):
|
|
14
|
+
"""
|
|
15
|
+
Class implementing model uncertainty -- i.e. uncertainties in pipe length, pipe roughness,
|
|
16
|
+
base demand, etc.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
pipe_length_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
21
|
+
Uncertainty of pipe lengths. None, in the case of no uncertainty.
|
|
22
|
+
|
|
23
|
+
The default is None.
|
|
24
|
+
pipe_roughness_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
25
|
+
Uncertainty of pipe roughness coefficients. None, in the case of no uncertainty.
|
|
26
|
+
|
|
27
|
+
The default is None.
|
|
28
|
+
pipe_diameter_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
29
|
+
Uncertainty of pipe diameters. None, in the case of no uncertainty.
|
|
30
|
+
|
|
31
|
+
The default is None.
|
|
32
|
+
demand_base_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
33
|
+
Uncertainty of base demands. None, in the case of no uncertainty.
|
|
34
|
+
|
|
35
|
+
The default is None.
|
|
36
|
+
demand_pattern_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
37
|
+
Uncertainty of demand patterns. None, in the case of no uncertainty.
|
|
38
|
+
|
|
39
|
+
The default is None.
|
|
40
|
+
elevation_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
41
|
+
Uncertainty of elevations. None, in the case of no uncertainty.
|
|
42
|
+
|
|
43
|
+
The default is None.
|
|
44
|
+
constants_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
45
|
+
Uncertainty of MSX constants. None, in the case of no uncertainty.
|
|
46
|
+
|
|
47
|
+
The default is None.
|
|
48
|
+
parameters_uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`, optional
|
|
49
|
+
Uncertainty of MSX parameters. None, in the case of no uncertainty.
|
|
50
|
+
|
|
51
|
+
The default is None.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(self, pipe_length_uncertainty: Uncertainty = None,
|
|
54
|
+
pipe_roughness_uncertainty: Uncertainty = None,
|
|
55
|
+
pipe_diameter_uncertainty: Uncertainty = None,
|
|
56
|
+
demand_base_uncertainty: Uncertainty = None,
|
|
57
|
+
demand_pattern_uncertainty: Uncertainty = None,
|
|
58
|
+
elevation_uncertainty: Uncertainty = None,
|
|
59
|
+
constants_uncertainty: Uncertainty = None,
|
|
60
|
+
parameters_uncertainty: Uncertainty = None, **kwds):
|
|
61
|
+
if pipe_length_uncertainty is not None:
|
|
62
|
+
if not isinstance(pipe_length_uncertainty, Uncertainty):
|
|
63
|
+
raise TypeError("'pipe_length_uncertainty' must be an instance of " +
|
|
64
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
65
|
+
f"'{type(pipe_length_uncertainty)}'")
|
|
66
|
+
if pipe_roughness_uncertainty is not None:
|
|
67
|
+
if not isinstance(pipe_roughness_uncertainty, Uncertainty):
|
|
68
|
+
raise TypeError("'pipe_roughness_uncertainty' must be an instance of " +
|
|
69
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
70
|
+
f"'{type(pipe_roughness_uncertainty)}'")
|
|
71
|
+
if pipe_diameter_uncertainty is not None:
|
|
72
|
+
if not isinstance(pipe_diameter_uncertainty, Uncertainty):
|
|
73
|
+
raise TypeError("'pipe_diameter_uncertainty' must be an instance of " +
|
|
74
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
75
|
+
f"'{type(pipe_diameter_uncertainty)}'")
|
|
76
|
+
if demand_base_uncertainty is not None:
|
|
77
|
+
if not isinstance(demand_base_uncertainty, Uncertainty):
|
|
78
|
+
raise TypeError("'demand_base_uncertainty' must be an instance of " +
|
|
79
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
80
|
+
f"'{type(demand_base_uncertainty)}'")
|
|
81
|
+
if demand_pattern_uncertainty is not None:
|
|
82
|
+
if not isinstance(demand_pattern_uncertainty, Uncertainty):
|
|
83
|
+
raise TypeError("'demand_pattern_uncertainty' must be an instance of " +
|
|
84
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
85
|
+
f"'{type(demand_pattern_uncertainty)}'")
|
|
86
|
+
if elevation_uncertainty is not None:
|
|
87
|
+
if not isinstance(elevation_uncertainty, Uncertainty):
|
|
88
|
+
raise TypeError("'elevation_uncertainty' must be an instance of " +
|
|
89
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
90
|
+
f"'{type(elevation_uncertainty)}'")
|
|
91
|
+
if constants_uncertainty is not None:
|
|
92
|
+
if not isinstance(constants_uncertainty, Uncertainty):
|
|
93
|
+
raise TypeError("'constants_uncertainty' must be an instance of " +
|
|
94
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
95
|
+
f"'{type(constants_uncertainty)}'")
|
|
96
|
+
if parameters_uncertainty is not None:
|
|
97
|
+
if not isinstance(parameters_uncertainty, Uncertainty):
|
|
98
|
+
raise TypeError("'parameters_uncertainty' must be an instance of " +
|
|
99
|
+
"'epyt_flow.uncertainty.Uncertainty' but not of " +
|
|
100
|
+
f"'{type(parameters_uncertainty)}'")
|
|
101
|
+
|
|
102
|
+
self.__pipe_length = pipe_length_uncertainty
|
|
103
|
+
self.__pipe_roughness = pipe_roughness_uncertainty
|
|
104
|
+
self.__pipe_diameter = pipe_diameter_uncertainty
|
|
105
|
+
self.__demand_base = demand_base_uncertainty
|
|
106
|
+
self.__demand_pattern = demand_pattern_uncertainty
|
|
107
|
+
self.__elevation = elevation_uncertainty
|
|
108
|
+
self.__constants = constants_uncertainty
|
|
109
|
+
self.__parameters = parameters_uncertainty
|
|
110
|
+
|
|
111
|
+
super().__init__(**kwds)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def pipe_length(self) -> Uncertainty:
|
|
115
|
+
"""
|
|
116
|
+
Gets the pipe length uncertainty.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
121
|
+
Pipe length uncertainty.
|
|
122
|
+
"""
|
|
123
|
+
return deepcopy(self.__pipe_length)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def pipe_roughness(self) -> Uncertainty:
|
|
127
|
+
"""
|
|
128
|
+
Gets the pipe roughness uncertainty.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
133
|
+
Pipe roughness uncertainty.
|
|
134
|
+
"""
|
|
135
|
+
return deepcopy(self.__pipe_roughness)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def pipe_diameter(self) -> Uncertainty:
|
|
139
|
+
"""
|
|
140
|
+
Gets the pipe diameter uncertainty.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
145
|
+
Pipe diameter uncertainty.
|
|
146
|
+
"""
|
|
147
|
+
return deepcopy(self.__pipe_diameter)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def demand_base(self) -> Uncertainty:
|
|
151
|
+
"""
|
|
152
|
+
Gets the demand base uncertainty.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
157
|
+
Demand base uncertainty.
|
|
158
|
+
"""
|
|
159
|
+
return deepcopy(self.__demand_base)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def demand_pattern(self) -> Uncertainty:
|
|
163
|
+
"""
|
|
164
|
+
Gets the demand pattern uncertainty.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
169
|
+
Demand pattern uncertainty.
|
|
170
|
+
"""
|
|
171
|
+
return deepcopy(self.__demand_pattern)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def elevation(self) -> Uncertainty:
|
|
175
|
+
"""
|
|
176
|
+
Gets the node elevation uncertainty.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
181
|
+
Node elevation uncertainty.
|
|
182
|
+
"""
|
|
183
|
+
return deepcopy(self.__elevation)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def constants(self) -> Uncertainty:
|
|
187
|
+
"""
|
|
188
|
+
Gets the MSX constant uncertainty.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
193
|
+
MSX constant uncertainty.
|
|
194
|
+
"""
|
|
195
|
+
return deepcopy(self.__constants)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def parameters(self) -> Uncertainty:
|
|
199
|
+
"""
|
|
200
|
+
Gets the MSX parameter uncertainty.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
205
|
+
MSX parameter uncertainty.
|
|
206
|
+
"""
|
|
207
|
+
return deepcopy(self.__parameters)
|
|
208
|
+
|
|
209
|
+
def get_attributes(self) -> dict:
|
|
210
|
+
return super().get_attributes() | {"pipe_length_uncertainty": self.__pipe_length,
|
|
211
|
+
"pipe_roughness_uncertainty": self.__pipe_roughness,
|
|
212
|
+
"pipe_diameter_uncertainty": self.__pipe_diameter,
|
|
213
|
+
"demand_base_uncertainty": self.__demand_base,
|
|
214
|
+
"demand_pattern_uncertainty": self.__demand_pattern,
|
|
215
|
+
"elevation_uncertainty": self.__elevation,
|
|
216
|
+
"constants_uncertainty": self.__constants,
|
|
217
|
+
"parameters_uncertainty": self.__parameters}
|
|
218
|
+
|
|
219
|
+
def __eq__(self, other) -> bool:
|
|
220
|
+
if not isinstance(other, ModelUncertainty):
|
|
221
|
+
raise TypeError("Can not compare 'ModelUncertainty' instance " +
|
|
222
|
+
f"with '{type(other)}' instance")
|
|
223
|
+
|
|
224
|
+
return self.__pipe_length == other.pipe_length \
|
|
225
|
+
and self.__pipe_roughness == other.pipe_roughness \
|
|
226
|
+
and self.__pipe_diameter == other.pipe_diameter \
|
|
227
|
+
and self.__demand_base == other.demand_base \
|
|
228
|
+
and self.__demand_pattern == other.demand_pattern \
|
|
229
|
+
and self.__elevation == other.elevation \
|
|
230
|
+
and self.__parameters == other.parameters and self.__constants == other.constants
|
|
231
|
+
|
|
232
|
+
def __str__(self) -> str:
|
|
233
|
+
return f"pipe_length: {self.__pipe_length} pipe_roughness: {self.__pipe_roughness} " + \
|
|
234
|
+
f"pipe_diameter: {self.__pipe_diameter} demand_base: {self.__demand_base} " + \
|
|
235
|
+
f"demand_pattern: {self.__demand_pattern} elevation: {self.__elevation} " + \
|
|
236
|
+
f"constants: {self.__constants} parameters: {self.__parameters}"
|
|
237
|
+
|
|
238
|
+
def apply(self, epanet_api: epyt.epanet) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Applies the specified model uncertainties to the scenario.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
epanet_api : `epyt.epanet`
|
|
245
|
+
Interface to EPANET and EPANET-MSX.
|
|
246
|
+
"""
|
|
247
|
+
if self.__pipe_length is not None:
|
|
248
|
+
link_length = epanet_api.getLinkLength()
|
|
249
|
+
link_length = self.__pipe_length.apply_batch(link_length)
|
|
250
|
+
epanet_api.setLinkLength(link_length)
|
|
251
|
+
|
|
252
|
+
if self.__pipe_diameter is not None:
|
|
253
|
+
link_diameters = epanet_api.getLinkDiameter()
|
|
254
|
+
link_diameters = self.__pipe_diameter.apply_batch(link_diameters)
|
|
255
|
+
epanet_api.setLinkDiameter(link_diameters)
|
|
256
|
+
|
|
257
|
+
if self.__pipe_roughness is not None:
|
|
258
|
+
coeffs = epanet_api.getLinkRoughnessCoeff()
|
|
259
|
+
coeffs = self.__pipe_roughness.apply_batch(coeffs)
|
|
260
|
+
epanet_api.setLinkRoughnessCoeff(coeffs)
|
|
261
|
+
|
|
262
|
+
if self.__demand_base is not None:
|
|
263
|
+
all_nodes_idx = epanet_api.getNodeIndex()
|
|
264
|
+
for node_idx in all_nodes_idx:
|
|
265
|
+
n_demand_categories = epanet_api.getNodeDemandCategoriesNumber(node_idx)
|
|
266
|
+
for demand_category in range(1, n_demand_categories):
|
|
267
|
+
base_demand = epanet_api.getNodeBaseDemands(node_idx)[demand_category]
|
|
268
|
+
base_demand = self.__demand_base.apply(base_demand)
|
|
269
|
+
epanet_api.setNodeBaseDemands(node_idx, demand_category, base_demand)
|
|
270
|
+
|
|
271
|
+
if self.__demand_pattern is not None:
|
|
272
|
+
demand_patterns_idx = epanet_api.getNodeDemandPatternIndex()
|
|
273
|
+
demand_patterns_id = np.unique([demand_patterns_idx[k]
|
|
274
|
+
for k in demand_patterns_idx.keys()])
|
|
275
|
+
for pattern_id in demand_patterns_id:
|
|
276
|
+
if pattern_id == 0:
|
|
277
|
+
continue
|
|
278
|
+
pattern_length = epanet_api.getPatternLengths(pattern_id)
|
|
279
|
+
for t in range(pattern_length):
|
|
280
|
+
v = epanet_api.getPatternValue(pattern_id, t+1)
|
|
281
|
+
epanet_api.setPatternValue(pattern_id, t+1, self.__demand_pattern.apply(v))
|
|
282
|
+
|
|
283
|
+
if self.__elevation is not None:
|
|
284
|
+
elevations = epanet_api.getNodeElevations()
|
|
285
|
+
elevations = self.__elevation.apply_batch(elevations)
|
|
286
|
+
epanet_api.setNodeElevations(elevations)
|
|
287
|
+
|
|
288
|
+
if self.__constants is not None:
|
|
289
|
+
constants = epanet_api.getMSXConstantsValue()
|
|
290
|
+
constants = self.__constants.apply_batch(constants)
|
|
291
|
+
epanet_api.setMSXConstantsValue(constants)
|
|
292
|
+
|
|
293
|
+
if self.__parameters is not None:
|
|
294
|
+
parameters_pipes = epanet_api.getMSXParametersPipesValue()
|
|
295
|
+
for i, pipe_idx in enumerate(epanet_api.getLinkPipeIndex()):
|
|
296
|
+
parameters_pipes[i] = self.__parameters.apply_batch(parameters_pipes[i])
|
|
297
|
+
epanet_api.setMSXParametersPipesValue(pipe_idx, parameters_pipes[i])
|
|
298
|
+
|
|
299
|
+
parameters_tanks = epanet_api.getMSXParametersTanksValue()
|
|
300
|
+
for i, tank_idx in enumerate(epanet_api.getNodeTankIndex()):
|
|
301
|
+
parameters_tanks[i] = self.__parameters.apply_batch(parameters_tanks[i])
|
|
302
|
+
epanet_api.setMSXParametersTanksValue(tank_idx, parameters_tanks[i])
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a class for implementing sensor noise (e.g. uncertainty in sensor readings).
|
|
3
|
+
"""
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
import numpy
|
|
6
|
+
|
|
7
|
+
from .uncertainties import Uncertainty
|
|
8
|
+
from ..serialization import serializable, JsonSerializable, SENSOR_NOISE_ID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@serializable(SENSOR_NOISE_ID, ".epytflow_sensor_noise")
|
|
12
|
+
class SensorNoise(JsonSerializable):
|
|
13
|
+
"""
|
|
14
|
+
Class implementing sensor noise/uncertainty.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
uncertainty : :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
19
|
+
Sensor uncertainty.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, uncertainty: Uncertainty, **kwds):
|
|
22
|
+
if not isinstance(uncertainty, Uncertainty):
|
|
23
|
+
raise TypeError("'uncertainty' must be an instance of " +
|
|
24
|
+
f"'epyt_flow.uncertainty.Uncertainty' not of {type(uncertainty)}")
|
|
25
|
+
|
|
26
|
+
self.__uncertainty = uncertainty
|
|
27
|
+
|
|
28
|
+
super().__init__(**kwds)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def uncertainty(self) -> Uncertainty:
|
|
32
|
+
"""
|
|
33
|
+
Gets the Sensor readings uncertainty.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
38
|
+
Sensor readings uncertainty.
|
|
39
|
+
"""
|
|
40
|
+
return deepcopy(self.__uncertainty)
|
|
41
|
+
|
|
42
|
+
def get_attributes(self) -> dict:
|
|
43
|
+
return super().get_attributes() | {"uncertainty": self.__uncertainty}
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other) -> bool:
|
|
46
|
+
if not isinstance(other, SensorNoise):
|
|
47
|
+
raise TypeError("Can not compare 'SensorNoise' instance " +
|
|
48
|
+
f"with '{type(other)}' instance")
|
|
49
|
+
|
|
50
|
+
return self.__uncertainty == other.uncertainty
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return f"uncertainty: {self.__uncertainty}"
|
|
54
|
+
|
|
55
|
+
def apply(self, sensor_readings: numpy.ndarray) -> numpy.ndarray:
|
|
56
|
+
"""
|
|
57
|
+
Applies the sensor uncertainty to given sensor readings -- i.e. sensor readings
|
|
58
|
+
are perturbed according to the specified uncertainty.
|
|
59
|
+
|
|
60
|
+
.. note::
|
|
61
|
+
Note that state sensor readings such as valve states, pump states, etc.
|
|
62
|
+
are NOT affected by sensor noise!
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
sensor_readings : `numpy.ndarray`
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
`numpy.ndarray`
|
|
71
|
+
Perturbed sensor readings.
|
|
72
|
+
"""
|
|
73
|
+
return self.__uncertainty.apply_batch(sensor_readings)
|