epyt-flow 0.15.0__py3-none-any.whl → 0.16.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/VERSION +1 -1
- epyt_flow/data/benchmarks/batadal.py +1 -1
- epyt_flow/gym/scenario_control_env.py +6 -12
- epyt_flow/serialization.py +19 -3
- epyt_flow/simulation/events/actuator_events.py +24 -24
- epyt_flow/simulation/events/leakages.py +45 -45
- epyt_flow/simulation/events/quality_events.py +23 -23
- epyt_flow/simulation/events/sensor_reading_attack.py +27 -27
- epyt_flow/simulation/events/sensor_reading_event.py +33 -33
- epyt_flow/simulation/scada/complex_control.py +103 -103
- epyt_flow/simulation/scada/scada_data.py +58 -30
- epyt_flow/simulation/scada/simple_control.py +33 -33
- epyt_flow/simulation/scenario_config.py +31 -0
- epyt_flow/simulation/scenario_simulator.py +217 -82
- epyt_flow/simulation/sensor_config.py +220 -108
- epyt_flow/topology.py +197 -6
- epyt_flow/uncertainty/model_uncertainty.py +23 -20
- epyt_flow/uncertainty/sensor_noise.py +16 -16
- epyt_flow/uncertainty/uncertainties.py +6 -4
- epyt_flow/utils.py +189 -30
- epyt_flow/visualization/scenario_visualizer.py +14 -5
- epyt_flow/visualization/visualization_utils.py +18 -16
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/METADATA +7 -5
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/RECORD +27 -27
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {epyt_flow-0.15.0.dist-info → epyt_flow-0.16.0.dist-info}/top_level.txt +0 -0
epyt_flow/topology.py
CHANGED
|
@@ -4,12 +4,13 @@ Module provides a class for representing the topology of WDN.
|
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
import warnings
|
|
6
6
|
from typing import Any
|
|
7
|
+
import math
|
|
7
8
|
import numpy as np
|
|
8
9
|
import networkx as nx
|
|
9
10
|
from scipy.sparse import bsr_array
|
|
10
11
|
from geopandas import GeoDataFrame
|
|
11
12
|
from shapely.geometry import Point, LineString
|
|
12
|
-
from epanet_plus import EpanetConstants
|
|
13
|
+
from epanet_plus import EpanetConstants, EPyT
|
|
13
14
|
|
|
14
15
|
from .serialization import serializable, JsonSerializable, NETWORK_TOPOLOGY_ID
|
|
15
16
|
|
|
@@ -67,6 +68,10 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
67
68
|
valves : `dict`
|
|
68
69
|
List of all valves -- i.e. valve ID, and information such as
|
|
69
70
|
valve type and connecting nodes.
|
|
71
|
+
curves : `dict[str, tuple[int, list[tuple[float, float]]]]`
|
|
72
|
+
All curves -- i.e. curve ID, and list of points.
|
|
73
|
+
patterns : `dict[str, list[float]]`
|
|
74
|
+
All time patterns -- i.e., pattern ID and list of multipliers.
|
|
70
75
|
units : `int`
|
|
71
76
|
Measurement units category -- i.e. US Customary or SI Metric.
|
|
72
77
|
|
|
@@ -79,16 +84,25 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
79
84
|
links: list[tuple[str, tuple[str, str], dict]],
|
|
80
85
|
pumps: dict,
|
|
81
86
|
valves: dict,
|
|
87
|
+
curves: dict[str, tuple[int, list[tuple[float, float]]]],
|
|
88
|
+
patterns: dict[str, list[float]],
|
|
82
89
|
units: int,
|
|
83
90
|
**kwds):
|
|
84
|
-
|
|
91
|
+
nx.Graph.__init__(self, name=f_inp, **kwds)
|
|
92
|
+
JsonSerializable.__init__(self)
|
|
85
93
|
|
|
86
94
|
self.__nodes = nodes
|
|
87
95
|
self.__links = links
|
|
88
96
|
self.__pumps = pumps
|
|
89
97
|
self.__valves = valves
|
|
98
|
+
self.__curves = curves
|
|
99
|
+
self.__patterns = patterns
|
|
90
100
|
self.__units = units
|
|
91
101
|
|
|
102
|
+
for key in self.__curves.keys(): # Fix value types -- tuple gets converted to list when deserializing it
|
|
103
|
+
self.__curves[key] = (self.__curves[key][0],
|
|
104
|
+
[tuple(value) for value in self.__curves[key][1]])
|
|
105
|
+
|
|
92
106
|
for node_id, node_info in nodes:
|
|
93
107
|
node_elevation = node_info["elevation"]
|
|
94
108
|
node_type = node_info["type"]
|
|
@@ -102,6 +116,141 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
102
116
|
info={"id": link_id, "type": link_type, "nodes": link,
|
|
103
117
|
"diameter": link_diameter, "length": link_length})
|
|
104
118
|
|
|
119
|
+
def to_inp_file(self, inp_file_out: str) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Creates an .inp file with the network layout and parameters as specified in
|
|
122
|
+
this instance.
|
|
123
|
+
Note that no control rules are set!
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
inp_file_out : `str`
|
|
128
|
+
Path to the .inp file.
|
|
129
|
+
"""
|
|
130
|
+
with EPyT(inp_file_in=inp_file_out, use_project=False) as epanet_api:
|
|
131
|
+
if self.units == UNITS_SIMETRIC:
|
|
132
|
+
epanet_api.setflowunits(EpanetConstants.EN_CMH)
|
|
133
|
+
else:
|
|
134
|
+
epanet_api.setflowunits(EpanetConstants.EN_GPM)
|
|
135
|
+
|
|
136
|
+
for curve_id, (curve_type, curve_data) in self.__curves.items():
|
|
137
|
+
epanet_api.addcurve(curve_id)
|
|
138
|
+
curve_idx = epanet_api.getcurveindex(curve_id)
|
|
139
|
+
epanet_api.setcurvetype(curve_idx, curve_type)
|
|
140
|
+
for i, (x, y) in enumerate(curve_data):
|
|
141
|
+
epanet_api.setcurvevalue(curve_idx, i+1, x, y)
|
|
142
|
+
|
|
143
|
+
for pattern_id, values in self.__patterns.items():
|
|
144
|
+
epanet_api.add_pattern(pattern_id, values)
|
|
145
|
+
|
|
146
|
+
for junc_id in self.get_all_junctions():
|
|
147
|
+
epanet_api.addnode(junc_id, EpanetConstants.EN_JUNCTION)
|
|
148
|
+
|
|
149
|
+
node_idx = epanet_api.get_node_idx(junc_id)
|
|
150
|
+
junc_info = self.get_node_info(junc_id)
|
|
151
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_ELEVATION,
|
|
152
|
+
junc_info["elevation"])
|
|
153
|
+
epanet_api.setbasedemand(node_idx, 1, junc_info["base_demand"])
|
|
154
|
+
epanet_api.setcoord(node_idx, junc_info["coord"][0], junc_info["coord"][1])
|
|
155
|
+
epanet_api.setcomment(EpanetConstants.EN_NODE, node_idx, junc_info["comment"])
|
|
156
|
+
if "demand_patterns_id" in junc_info:
|
|
157
|
+
for i, demand_pattern_id in enumerate(junc_info["demand_patterns_id"]):
|
|
158
|
+
epanet_api.setdemandpattern(node_idx, i+1,
|
|
159
|
+
epanet_api.getpatternindex(demand_pattern_id))
|
|
160
|
+
|
|
161
|
+
for reservoir_id in self.get_all_reservoirs():
|
|
162
|
+
epanet_api.addnode(reservoir_id, EpanetConstants.EN_RESERVOIR)
|
|
163
|
+
|
|
164
|
+
node_idx = epanet_api.get_node_idx(reservoir_id)
|
|
165
|
+
reservoir_info = self.get_node_info(reservoir_id)
|
|
166
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_ELEVATION,
|
|
167
|
+
reservoir_info["elevation"])
|
|
168
|
+
epanet_api.setcoord(node_idx, reservoir_info["coord"][0],
|
|
169
|
+
reservoir_info["coord"][1])
|
|
170
|
+
epanet_api.setcomment(EpanetConstants.EN_NODE, node_idx,
|
|
171
|
+
reservoir_info["comment"])
|
|
172
|
+
|
|
173
|
+
for tank_id in self.get_all_tanks():
|
|
174
|
+
epanet_api.addnode(tank_id, EpanetConstants.EN_TANK)
|
|
175
|
+
|
|
176
|
+
node_idx = epanet_api.get_node_idx(tank_id)
|
|
177
|
+
tank_info = self.get_node_info(tank_id)
|
|
178
|
+
if tank_info["cylindric"] is False:
|
|
179
|
+
raise NotImplementedError("Non-cylindric tanks are not supported!")
|
|
180
|
+
else:
|
|
181
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_ELEVATION,
|
|
182
|
+
tank_info["elevation"])
|
|
183
|
+
epanet_api.setcoord(node_idx, tank_info["coord"][0], tank_info["coord"][1])
|
|
184
|
+
epanet_api.setcomment(EpanetConstants.EN_NODE, node_idx, tank_info["comment"])
|
|
185
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_TANKDIAM,
|
|
186
|
+
tank_info["diameter"])
|
|
187
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_MIXFRACTION,
|
|
188
|
+
tank_info["mixing_fraction"])
|
|
189
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_MIXMODEL,
|
|
190
|
+
tank_info["mixing_model"])
|
|
191
|
+
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_CANOVERFLOW,
|
|
192
|
+
float(tank_info["can_overflow"]))
|
|
193
|
+
|
|
194
|
+
tank_info["min_level"] = tank_info["min_vol"] / \
|
|
195
|
+
(math.pi * (0.5 * tank_info["diameter"])**2)
|
|
196
|
+
tank_info["init_level"] = tank_info["init_vol"] / \
|
|
197
|
+
(math.pi * (0.5 * tank_info["diameter"])**2)
|
|
198
|
+
|
|
199
|
+
epanet_api.settankdata(node_idx, tank_info["elevation"],
|
|
200
|
+
tank_info["init_level"], tank_info["min_level"],
|
|
201
|
+
tank_info["max_level"], tank_info["diameter"],
|
|
202
|
+
tank_info["min_vol"], tank_info["vol_curve_id"])
|
|
203
|
+
|
|
204
|
+
for pipe_id, (node_a, node_b) in self.get_all_pipes():
|
|
205
|
+
epanet_api.addlink(pipe_id, EpanetConstants.EN_PIPE, node_a, node_b)
|
|
206
|
+
|
|
207
|
+
pipe_idx = epanet_api.get_link_idx(pipe_id)
|
|
208
|
+
pipe_info = self.get_link_info(pipe_id)
|
|
209
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_LENGTH, pipe_info["length"])
|
|
210
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_DIAMETER,
|
|
211
|
+
pipe_info["diameter"])
|
|
212
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_ROUGHNESS,
|
|
213
|
+
pipe_info["roughness_coeff"])
|
|
214
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_KBULK, pipe_info["bulk_coeff"])
|
|
215
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_KWALL, pipe_info["wall_coeff"])
|
|
216
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_MINORLOSS,
|
|
217
|
+
pipe_info["loss_coeff"])
|
|
218
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_INITSETTING,
|
|
219
|
+
pipe_info["init_setting"])
|
|
220
|
+
epanet_api.setlinkvalue(pipe_idx, EpanetConstants.EN_INITSTATUS,
|
|
221
|
+
pipe_info["init_status"])
|
|
222
|
+
|
|
223
|
+
for valve_id in self.get_all_valves():
|
|
224
|
+
valve_info = self.get_valve_info(valve_id)
|
|
225
|
+
node_a, node_b = valve_info["end_points"]
|
|
226
|
+
epanet_api.addlink(valve_id, valve_info["type"], node_a, node_b)
|
|
227
|
+
link_idx = epanet_api.get_link_idx(valve_id)
|
|
228
|
+
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_DIAMETER,
|
|
229
|
+
valve_info["diameter"])
|
|
230
|
+
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_INITSETTING,
|
|
231
|
+
valve_info["initial_setting"])
|
|
232
|
+
if valve_info["type"] not in [EpanetConstants.EN_GPV, EpanetConstants.EN_PRV,
|
|
233
|
+
EpanetConstants.EN_CVPIPE]:
|
|
234
|
+
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_INITSTATUS,
|
|
235
|
+
valve_info["initial_status"])
|
|
236
|
+
|
|
237
|
+
for pump_id in self.get_all_pumps():
|
|
238
|
+
pump_info = self.get_pump_info(pump_id)
|
|
239
|
+
node_a, node_b = pump_info["end_points"]
|
|
240
|
+
epanet_api.addlink(pump_id, pump_info["type"], node_a, node_b)
|
|
241
|
+
|
|
242
|
+
link_idx = link_idx = epanet_api.get_link_idx(pump_id)
|
|
243
|
+
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_INITSETTING,
|
|
244
|
+
pump_info["init_setting"])
|
|
245
|
+
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_INITSTATUS,
|
|
246
|
+
pump_info["init_status"])
|
|
247
|
+
|
|
248
|
+
if pump_info["curve_id"] is not None:
|
|
249
|
+
curve_idx = epanet_api.getcurveindex(pump_info["curve_id"])
|
|
250
|
+
epanet_api.setheadcurveindex(link_idx, curve_idx)
|
|
251
|
+
|
|
252
|
+
epanet_api.saveinpfile(inp_file_out)
|
|
253
|
+
|
|
105
254
|
def convert_units(self, units: int) -> Any:
|
|
106
255
|
"""
|
|
107
256
|
Converts this instance to a :class:`~epyt_flow.topology.NetworkTopology` instance
|
|
@@ -138,6 +287,7 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
138
287
|
# Get all data and convert units
|
|
139
288
|
inch_to_millimeter = 25.4
|
|
140
289
|
feet_to_meter = 0.3048
|
|
290
|
+
cubicmeter_to_cubicfeet = 35.3146667215
|
|
141
291
|
|
|
142
292
|
nodes = []
|
|
143
293
|
for node_id in self.get_all_nodes():
|
|
@@ -146,10 +296,23 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
146
296
|
conv_factor = 1. / feet_to_meter
|
|
147
297
|
else:
|
|
148
298
|
conv_factor = feet_to_meter
|
|
299
|
+
|
|
149
300
|
node_info["elevation"] *= conv_factor
|
|
150
301
|
if "diameter" in node_info:
|
|
151
302
|
node_info["diameter"] *= conv_factor
|
|
152
303
|
|
|
304
|
+
if "max_level" in node_info:
|
|
305
|
+
node_info["max_level"] *= conv_factor
|
|
306
|
+
if "min_level" in node_info:
|
|
307
|
+
node_info["min_level"] *= conv_factor
|
|
308
|
+
if "init_level" in node_info:
|
|
309
|
+
node_info["init_level"] *= conv_factor
|
|
310
|
+
if "min_vol" in node_info:
|
|
311
|
+
if units == UNITS_USCUSTOM:
|
|
312
|
+
node_info["min_vol"] *= cubicmeter_to_cubicfeet
|
|
313
|
+
else:
|
|
314
|
+
node_info["min_vol"] *= 1. / cubicmeter_to_cubicfeet
|
|
315
|
+
|
|
153
316
|
nodes.append((node_id, node_info))
|
|
154
317
|
|
|
155
318
|
links = []
|
|
@@ -171,7 +334,8 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
171
334
|
links.append((link_id, link_nodes, link_info))
|
|
172
335
|
|
|
173
336
|
return NetworkTopology(f_inp=self.name, nodes=nodes, links=links, pumps=self.pumps,
|
|
174
|
-
valves=self.valves, units=units
|
|
337
|
+
valves=self.valves, units=units, curves=self.__curves,
|
|
338
|
+
patterns=self.__patterns)
|
|
175
339
|
|
|
176
340
|
def get_all_nodes(self) -> list[str]:
|
|
177
341
|
"""
|
|
@@ -455,6 +619,30 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
455
619
|
else:
|
|
456
620
|
raise ValueError(f"Unknown valve: '{valve_id}'")
|
|
457
621
|
|
|
622
|
+
@property
|
|
623
|
+
def curves(self) -> dict[str, tuple[int, list[tuple[float, float]]]]:
|
|
624
|
+
"""
|
|
625
|
+
Gets all curves -- i.e., ID and list of points.
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
`dict[str, tuple[int, list[tuple[float, float]]]]`
|
|
630
|
+
All curves.
|
|
631
|
+
"""
|
|
632
|
+
return deepcopy(self.__curves)
|
|
633
|
+
|
|
634
|
+
@property
|
|
635
|
+
def patterns(self) -> dict[str, list[float]]:
|
|
636
|
+
"""
|
|
637
|
+
Returns all time patterns -- i.e., ID and list of multipliers.
|
|
638
|
+
|
|
639
|
+
Returns
|
|
640
|
+
-------
|
|
641
|
+
`dict[str, list[float]]`
|
|
642
|
+
All time patterns.
|
|
643
|
+
"""
|
|
644
|
+
return deepcopy(self.__patterns)
|
|
645
|
+
|
|
458
646
|
@property
|
|
459
647
|
def pumps(self) -> dict:
|
|
460
648
|
"""
|
|
@@ -504,8 +692,7 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
504
692
|
adj_matrix = self.get_adj_matrix()
|
|
505
693
|
other_adj_matrix = other.get_adj_matrix()
|
|
506
694
|
|
|
507
|
-
return
|
|
508
|
-
self.name == other.name \
|
|
695
|
+
return self.name == other.name \
|
|
509
696
|
and not np.any(adj_matrix.data != other_adj_matrix.data) \
|
|
510
697
|
and not np.any(adj_matrix.indices != other_adj_matrix.indices) \
|
|
511
698
|
and not np.any(adj_matrix.indptr != other_adj_matrix.indptr) \
|
|
@@ -514,7 +701,9 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
514
701
|
for link_a, link_b in zip(self.get_all_links(), other.get_all_links())) \
|
|
515
702
|
and self.__units == other.units \
|
|
516
703
|
and self.get_all_pumps() == other.get_all_pumps() \
|
|
517
|
-
and self.get_all_valves() == other.get_all_valves()
|
|
704
|
+
and self.get_all_valves() == other.get_all_valves() \
|
|
705
|
+
and self.__curves == other.curves \
|
|
706
|
+
and self.__patterns == other.patterns
|
|
518
707
|
|
|
519
708
|
def __str__(self) -> str:
|
|
520
709
|
return f"f_inp: {self.name} nodes: {self.__nodes} links: {self.__links} " +\
|
|
@@ -527,6 +716,8 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
527
716
|
"links": self.__links,
|
|
528
717
|
"pumps": self.__pumps,
|
|
529
718
|
"valves": self.__valves,
|
|
719
|
+
"curves": self.__curves,
|
|
720
|
+
"patterns": self.__patterns,
|
|
530
721
|
"units": self.__units}
|
|
531
722
|
|
|
532
723
|
def to_gis(self, coord_reference_system: str = None, pumps_as_points: bool = False,
|
|
@@ -328,6 +328,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
328
328
|
self._cache_msx_tanks_parameters = None
|
|
329
329
|
self._cache_msx_patterns = None
|
|
330
330
|
|
|
331
|
+
self.__np_rand_gen = np.random.default_rng(seed=self.__seed)
|
|
332
|
+
|
|
331
333
|
super().__init__(**kwds)
|
|
332
334
|
|
|
333
335
|
@property
|
|
@@ -703,13 +705,12 @@ class ModelUncertainty(JsonSerializable):
|
|
|
703
705
|
epanet_api : `epanet_plus.EPyT <https://epanet-plus.readthedocs.io/en/stable/api.html#epanet_plus.epanet_toolkit.EPyT>`_
|
|
704
706
|
Interface to EPANET and EPANET-MSX.
|
|
705
707
|
"""
|
|
706
|
-
np_rand_gen = np.random.default_rng(seed=self.__seed)
|
|
707
708
|
|
|
708
709
|
all_links_idx = epanet_api.get_all_links_idx()
|
|
709
710
|
all_nodes_idx = epanet_api.get_all_nodes_idx()
|
|
710
711
|
|
|
711
712
|
if self._global_pipe_length is not None:
|
|
712
|
-
self._global_pipe_length.set_random_generator(
|
|
713
|
+
self._global_pipe_length.set_random_generator(self.__np_rand_gen)
|
|
713
714
|
|
|
714
715
|
link_length = np.array([epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_LENGTH)
|
|
715
716
|
for link_idx in all_links_idx])
|
|
@@ -720,7 +721,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
720
721
|
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_LENGTH, link_value)
|
|
721
722
|
|
|
722
723
|
if self._local_pipe_length is not None:
|
|
723
|
-
self._local_pipe_length.set_random_generator(
|
|
724
|
+
self._local_pipe_length.set_random_generator(self.__np_rand_gen)
|
|
724
725
|
self._cache_links_length = np.array([epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_LENGTH)
|
|
725
726
|
for link_idx in all_links_idx])
|
|
726
727
|
|
|
@@ -732,7 +733,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
732
733
|
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_LENGTH, link_length)
|
|
733
734
|
|
|
734
735
|
if self._global_pipe_diameter is not None:
|
|
735
|
-
self._global_pipe_diameter.set_random_generator(
|
|
736
|
+
self._global_pipe_diameter.set_random_generator(self.__np_rand_gen)
|
|
736
737
|
|
|
737
738
|
link_diameters = np.array([epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_DIAMETER)
|
|
738
739
|
for link_idx in all_links_idx])
|
|
@@ -743,7 +744,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
743
744
|
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_DIAMETER, link_value)
|
|
744
745
|
|
|
745
746
|
if self._local_pipe_diameter is not None:
|
|
746
|
-
self._local_pipe_diameter.set_random_generator(
|
|
747
|
+
self._local_pipe_diameter.set_random_generator(self.__np_rand_gen)
|
|
747
748
|
self._cache_links_diameter = np.array([epanet_api.getlinkvalue(link_idx,
|
|
748
749
|
EpanetConstants.EN_DIAMETER)
|
|
749
750
|
for link_idx in all_links_idx])
|
|
@@ -756,7 +757,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
756
757
|
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_DIAMETER, link_diameter)
|
|
757
758
|
|
|
758
759
|
if self._global_pipe_roughness is not None:
|
|
759
|
-
self._global_pipe_roughness.set_random_generator(
|
|
760
|
+
self._global_pipe_roughness.set_random_generator(self.__np_rand_gen)
|
|
760
761
|
|
|
761
762
|
coeffs = np.array([epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_ROUGHNESS)
|
|
762
763
|
for link_idx in all_links_idx])
|
|
@@ -767,7 +768,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
767
768
|
epanet_api.setlinkvalue(link_idx, EpanetConstants.EN_ROUGHNESS, link_value)
|
|
768
769
|
|
|
769
770
|
if self._local_pipe_roughness is not None:
|
|
770
|
-
self._local_pipe_roughness.set_random_generator(
|
|
771
|
+
self._local_pipe_roughness.set_random_generator(self.__np_rand_gen)
|
|
771
772
|
self._cache_links_roughness_coeff = \
|
|
772
773
|
np.array([epanet_api.getlinkvalue(link_idx, EpanetConstants.EN_ROUGHNESS)
|
|
773
774
|
for link_idx in all_links_idx])
|
|
@@ -782,7 +783,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
782
783
|
link_roughness_coeff)
|
|
783
784
|
|
|
784
785
|
if self._global_base_demand is not None:
|
|
785
|
-
self._global_base_demand.set_random_generator(
|
|
786
|
+
self._global_base_demand.set_random_generator(self.__np_rand_gen)
|
|
786
787
|
|
|
787
788
|
self._cache_nodes_base_demand = {}
|
|
788
789
|
for node_idx in all_nodes_idx:
|
|
@@ -796,7 +797,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
796
797
|
epanet_api.setbasedemand(node_idx, demand_idx + 1, base_demand)
|
|
797
798
|
|
|
798
799
|
if self._local_base_demand is not None:
|
|
799
|
-
self._local_base_demand.set_random_generator(
|
|
800
|
+
self._local_base_demand.set_random_generator(self.__np_rand_gen)
|
|
800
801
|
|
|
801
802
|
self._cache_nodes_base_demand = {}
|
|
802
803
|
for node_id, uncertainty in self._local_base_demand.items():
|
|
@@ -811,14 +812,16 @@ class ModelUncertainty(JsonSerializable):
|
|
|
811
812
|
epanet_api.setbasedemand(node_idx, demand_idx + 1, base_demand)
|
|
812
813
|
|
|
813
814
|
if self._global_demand_pattern is not None:
|
|
814
|
-
self._global_demand_pattern.set_random_generator(
|
|
815
|
+
self._global_demand_pattern.set_random_generator(self.__np_rand_gen)
|
|
815
816
|
|
|
816
817
|
self._cache_nodes_demand_pattern = {}
|
|
817
818
|
demand_patterns_idx = np.array([epanet_api.getdemandpattern(node_idx, demand_idx + 1)
|
|
818
819
|
for node_idx in epanet_api.get_all_nodes_idx()
|
|
819
820
|
for demand_idx in range(epanet_api.getnumdemands(node_idx))])
|
|
820
821
|
|
|
821
|
-
|
|
822
|
+
demand_patterns_idx = set(demand_patterns_idx)
|
|
823
|
+
demand_patterns_idx.discard(0)
|
|
824
|
+
for pattern_idx in list(demand_patterns_idx):
|
|
822
825
|
demand_pattern = np.array(epanet_api.get_pattern(pattern_idx))
|
|
823
826
|
self._cache_nodes_demand_pattern[pattern_idx] = np.copy(demand_pattern)
|
|
824
827
|
|
|
@@ -826,7 +829,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
826
829
|
epanet_api.set_pattern(pattern_idx, demand_pattern.tolist())
|
|
827
830
|
|
|
828
831
|
if self._local_demand_pattern is not None:
|
|
829
|
-
self._local_demand_pattern.set_random_generator(
|
|
832
|
+
self._local_demand_pattern.set_random_generator(self.__np_rand_gen)
|
|
830
833
|
|
|
831
834
|
self._cache_nodes_demand_pattern = {}
|
|
832
835
|
|
|
@@ -839,7 +842,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
839
842
|
epanet_api.set_pattern(pattern_idx, demand_pattern.tolist())
|
|
840
843
|
|
|
841
844
|
if self._global_elevation is not None:
|
|
842
|
-
self._global_elevation.set_random_generator(
|
|
845
|
+
self._global_elevation.set_random_generator(self.__np_rand_gen)
|
|
843
846
|
|
|
844
847
|
elevations = np.array([epanet_api.get_node_elevation(node_idx)
|
|
845
848
|
for node_idx in epanet_api.get_all_nodes_idx()])
|
|
@@ -850,7 +853,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
850
853
|
epanet_api.setnodevalue(node_idx + 1, EpanetConstants.EN_ELEVATION, node_elev)
|
|
851
854
|
|
|
852
855
|
if self._local_elevation is not None:
|
|
853
|
-
self._local_elevation.set_random_generator(
|
|
856
|
+
self._local_elevation.set_random_generator(self.__np_rand_gen)
|
|
854
857
|
self._cache_nodes_elevation = np.array([epanet_api.get_node_elevation(node_idx)
|
|
855
858
|
for node_idx in epanet_api.get_all_nodes_idx()])
|
|
856
859
|
|
|
@@ -862,7 +865,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
862
865
|
epanet_api.setnodevalue(node_idx, EpanetConstants.EN_ELEVATION, elevation)
|
|
863
866
|
|
|
864
867
|
if self._local_patterns is not None:
|
|
865
|
-
self._local_patterns.set_random_generator(
|
|
868
|
+
self._local_patterns.set_random_generator(self.__np_rand_gen)
|
|
866
869
|
self._cache_patterns = {}
|
|
867
870
|
|
|
868
871
|
for pattern_id, uncertainty in self._local_patterns.items():
|
|
@@ -875,7 +878,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
875
878
|
|
|
876
879
|
if epanet_api.msx_file is not None:
|
|
877
880
|
if self._global_constants is not None:
|
|
878
|
-
self._global_constants.set_random_generator(
|
|
881
|
+
self._global_constants.set_random_generator(self.__np_rand_gen)
|
|
879
882
|
|
|
880
883
|
constants = np.array([epanet_api.MSXgetconstant(const_idx + 1)
|
|
881
884
|
for const_idx in range(epanet_api.MSXgetcount(EpanetConstants.MSX_CONSTANT))])
|
|
@@ -886,7 +889,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
886
889
|
epanet_api.MSXsetconstant(const_idx + 1, const_value)
|
|
887
890
|
|
|
888
891
|
if self._local_constants:
|
|
889
|
-
self._local_constants.set_random_generator(
|
|
892
|
+
self._local_constants.set_random_generator(self.__np_rand_gen)
|
|
890
893
|
|
|
891
894
|
self._cache_msx_patterns = np.array([epanet_api.MSXgetconstant(const_idx + 1)
|
|
892
895
|
for const_idx in range(epanet_api.MSXgetcount(EpanetConstants.MSX_CONSTANT))])
|
|
@@ -899,7 +902,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
899
902
|
epanet_api.MSXsetconstant(idx, constant)
|
|
900
903
|
|
|
901
904
|
if self._global_parameters is not None:
|
|
902
|
-
self._global_parameters.set_random_generator(
|
|
905
|
+
self._global_parameters.set_random_generator(self.__np_rand_gen)
|
|
903
906
|
|
|
904
907
|
self._cache_msx_links_parameters = {}
|
|
905
908
|
num_params = epanet_api.MSXgetcount(EpanetConstants.MSX_PARAMETER)
|
|
@@ -934,7 +937,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
934
937
|
epanet_api.MSXsetparameter(EpanetConstants.MSX_NODE, tank_idx, idx + 1, val)
|
|
935
938
|
|
|
936
939
|
if self._local_parameters is not None:
|
|
937
|
-
self._local_parameters.set_random_generator(
|
|
940
|
+
self._local_parameters.set_random_generator(self.__np_rand_gen)
|
|
938
941
|
self._cache_msx_links_parameters = {}
|
|
939
942
|
self._cache_msx_tanks_parameters = {}
|
|
940
943
|
|
|
@@ -959,7 +962,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
959
962
|
epanet_api.MSXsetparameter(item_type, item_idx, idx, parameter)
|
|
960
963
|
|
|
961
964
|
if self._local_msx_patterns is not None:
|
|
962
|
-
self._local_msx_patterns.set_random_generator(
|
|
965
|
+
self._local_msx_patterns.set_random_generator(self.__np_rand_gen)
|
|
963
966
|
self._cache_msx_patterns = {}
|
|
964
967
|
|
|
965
968
|
for pattern_id, uncertainty in self._local_msx_patterns.items():
|
|
@@ -50,9 +50,9 @@ class SensorNoise(JsonSerializable):
|
|
|
50
50
|
raise TypeError("'local_uncertainties' must be an instance of " +
|
|
51
51
|
"'dict[tuple[int, str], epyt_flow.uncertainty.Uncertainty]'")
|
|
52
52
|
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
53
|
+
self._global_uncertainty = global_uncertainty
|
|
54
|
+
self._local_uncertainties = local_uncertainties
|
|
55
|
+
self._np_rand_gen = default_rng(seed)
|
|
56
56
|
|
|
57
57
|
super().__init__(**kwds)
|
|
58
58
|
|
|
@@ -66,7 +66,7 @@ class SensorNoise(JsonSerializable):
|
|
|
66
66
|
:class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`
|
|
67
67
|
Global sensor readings uncertainty.
|
|
68
68
|
"""
|
|
69
|
-
return deepcopy(self.
|
|
69
|
+
return deepcopy(self._global_uncertainty)
|
|
70
70
|
|
|
71
71
|
@property
|
|
72
72
|
def local_uncertainties(self) -> dict[int, str, Uncertainty]:
|
|
@@ -78,23 +78,23 @@ class SensorNoise(JsonSerializable):
|
|
|
78
78
|
dict[tuple[int, str], :class:`~epyt_flow.uncertainty.uncertainties.Uncertainty`]
|
|
79
79
|
Local (i.e. sensor specific) uncertainties.
|
|
80
80
|
"""
|
|
81
|
-
return deepcopy(self.
|
|
81
|
+
return deepcopy(self._local_uncertainties)
|
|
82
82
|
|
|
83
83
|
def get_attributes(self) -> dict:
|
|
84
|
-
return super().get_attributes() | {"global_uncertainty": self.
|
|
85
|
-
"local_uncertainties": self.
|
|
84
|
+
return super().get_attributes() | {"global_uncertainty": self._global_uncertainty,
|
|
85
|
+
"local_uncertainties": self._local_uncertainties}
|
|
86
86
|
|
|
87
87
|
def __eq__(self, other) -> bool:
|
|
88
88
|
if not isinstance(other, SensorNoise):
|
|
89
89
|
raise TypeError("Can not compare 'SensorNoise' instance " +
|
|
90
90
|
f"with '{type(other)}' instance")
|
|
91
91
|
|
|
92
|
-
return super().__eq__(other) and self.
|
|
93
|
-
self.
|
|
92
|
+
return super().__eq__(other) and self._global_uncertainty == other.global_uncertainty and \
|
|
93
|
+
self._local_uncertainties == other.local_uncertainties
|
|
94
94
|
|
|
95
95
|
def __str__(self) -> str:
|
|
96
|
-
return f"global_uncertainty: {self.
|
|
97
|
-
f"local_uncertainties: {self.
|
|
96
|
+
return f"global_uncertainty: {self._global_uncertainty} " + \
|
|
97
|
+
f"local_uncertainties: {self._local_uncertainties}"
|
|
98
98
|
|
|
99
99
|
def apply_local_uncertainty(self, map_sensor_to_idx: Callable[[int, str], int],
|
|
100
100
|
sensor_readings: numpy.ndarray) -> numpy.ndarray:
|
|
@@ -115,10 +115,10 @@ class SensorNoise(JsonSerializable):
|
|
|
115
115
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
116
116
|
Perturbed sensor readings.
|
|
117
117
|
"""
|
|
118
|
-
if self.
|
|
118
|
+
if self._local_uncertainties is None:
|
|
119
119
|
return sensor_readings
|
|
120
120
|
else:
|
|
121
|
-
self.
|
|
121
|
+
self._local_uncertainties.set_random_generator(self._np_rand_gen)
|
|
122
122
|
|
|
123
123
|
for (sensor_type, sensor_id), uncertainty in map_sensor_to_idx.items():
|
|
124
124
|
idx = map_sensor_to_idx(sensor_type, sensor_id)
|
|
@@ -144,9 +144,9 @@ class SensorNoise(JsonSerializable):
|
|
|
144
144
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
145
145
|
Perturbed sensor readings.
|
|
146
146
|
"""
|
|
147
|
-
if self.
|
|
147
|
+
if self._global_uncertainty is None:
|
|
148
148
|
return sensor_readings
|
|
149
149
|
else:
|
|
150
|
-
self.
|
|
150
|
+
self._global_uncertainty.set_random_generator(self._np_rand_gen)
|
|
151
151
|
|
|
152
|
-
return self.
|
|
152
|
+
return self._global_uncertainty.apply_batch(sensor_readings)
|
|
@@ -101,9 +101,11 @@ class Uncertainty(ABC):
|
|
|
101
101
|
return {"min_value": self.__min_value, "max_value": self.__max_value}
|
|
102
102
|
|
|
103
103
|
def __eq__(self, other) -> bool:
|
|
104
|
-
if not isinstance(other, Uncertainty):
|
|
104
|
+
if not isinstance(other, (Uncertainty, type(None))):
|
|
105
105
|
raise TypeError("Can not compare 'Uncertainty' instance " +
|
|
106
106
|
f"with '{type(other)}' instance")
|
|
107
|
+
if other is None:
|
|
108
|
+
return False
|
|
107
109
|
|
|
108
110
|
return self.__min_value == other.min_value and self.__max_value == other.max_value and \
|
|
109
111
|
self._random_generator == other.random_generator
|
|
@@ -221,7 +223,7 @@ class GaussianUncertainty(Uncertainty):
|
|
|
221
223
|
return super().get_attributes() | {"mean": self._mean, "scale": self._scale}
|
|
222
224
|
|
|
223
225
|
def __eq__(self, other) -> bool:
|
|
224
|
-
if not isinstance(other, GaussianUncertainty):
|
|
226
|
+
if not isinstance(other, (GaussianUncertainty, type(None))):
|
|
225
227
|
raise TypeError("Can not compare 'GaussianUncertainty' instance " +
|
|
226
228
|
f"with '{type(other)}' instance")
|
|
227
229
|
|
|
@@ -320,7 +322,7 @@ class UniformUncertainty(Uncertainty):
|
|
|
320
322
|
return super().get_attributes() | {"low": self.__low, "high": self.__high}
|
|
321
323
|
|
|
322
324
|
def __eq__(self, other) -> bool:
|
|
323
|
-
if not isinstance(other, UniformUncertainty):
|
|
325
|
+
if not isinstance(other, (UniformUncertainty, type(None))):
|
|
324
326
|
raise TypeError("Can not compare 'UniformUncertainty' instance " +
|
|
325
327
|
f"with '{type(other)}' instance")
|
|
326
328
|
|
|
@@ -562,7 +564,7 @@ class DeepUncertainty(Uncertainty):
|
|
|
562
564
|
"max_noise_value": self.__max_noise_value}
|
|
563
565
|
|
|
564
566
|
def __eq__(self, other) -> bool:
|
|
565
|
-
if not isinstance(other, DeepUncertainty):
|
|
567
|
+
if not isinstance(other, (DeepUncertainty, type(None))):
|
|
566
568
|
raise TypeError("Can not compare 'DeepUncertainty' instance " +
|
|
567
569
|
f"with '{type(other)}' instance")
|
|
568
570
|
|