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
|
@@ -10,7 +10,8 @@ import matplotlib
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
from epanet_plus import EpanetConstants
|
|
12
12
|
|
|
13
|
-
from ..sensor_config import SensorConfig,
|
|
13
|
+
from ..sensor_config import SensorConfig, valid_sensor_types, \
|
|
14
|
+
is_flowunit_simetric, massunit_to_str, flowunit_to_str,\
|
|
14
15
|
qualityunit_to_str, areaunit_to_str,\
|
|
15
16
|
MASS_UNIT_MG, MASS_UNIT_UG, TIME_UNIT_HRS, MASS_UNIT_MOL, MASS_UNIT_MMOL, \
|
|
16
17
|
AREA_UNIT_CM2, AREA_UNIT_FT2, AREA_UNIT_M2, \
|
|
@@ -22,7 +23,7 @@ from ..sensor_config import SensorConfig, is_flowunit_simetric, massunit_to_str,
|
|
|
22
23
|
from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
|
|
23
24
|
from ...uncertainty import SensorNoise
|
|
24
25
|
from ...serialization import serializable, Serializable, SCADA_DATA_ID
|
|
25
|
-
from ...topology import NetworkTopology
|
|
26
|
+
from ...topology import NetworkTopology, UNITS_USCUSTOM, UNITS_SIMETRIC
|
|
26
27
|
from ...utils import plot_timeseries_data
|
|
27
28
|
|
|
28
29
|
|
|
@@ -1056,7 +1057,15 @@ class ScadaData(Serializable):
|
|
|
1056
1057
|
surface_species_mass_unit=new_surface_species_mass_unit,
|
|
1057
1058
|
surface_species_area_unit=new_surface_species_area_unit)
|
|
1058
1059
|
|
|
1059
|
-
|
|
1060
|
+
if flow_unit is not None:
|
|
1061
|
+
if is_flowunit_simetric(flow_unit):
|
|
1062
|
+
network_topo = self.network_topo.convert_units(UNITS_SIMETRIC)
|
|
1063
|
+
else:
|
|
1064
|
+
network_topo = self.network_topo.convert_units(UNITS_USCUSTOM)
|
|
1065
|
+
else:
|
|
1066
|
+
network_topo = self.network_topo
|
|
1067
|
+
|
|
1068
|
+
return ScadaData(network_topo=network_topo,
|
|
1060
1069
|
warnings_code=self.warnings_code,
|
|
1061
1070
|
sensor_config=sensor_config,
|
|
1062
1071
|
sensor_readings_time=self.sensor_readings_time,
|
|
@@ -2141,33 +2150,52 @@ class ScadaData(Serializable):
|
|
|
2141
2150
|
else:
|
|
2142
2151
|
data = []
|
|
2143
2152
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2153
|
+
for sensor_type in self.__sensor_config.sensor_ordering:
|
|
2154
|
+
if sensor_type==SENSOR_TYPE_NODE_PRESSURE \
|
|
2155
|
+
and self.__pressure_data_raw is not None:
|
|
2156
|
+
data.append(self.__pressure_data_raw)
|
|
2157
|
+
elif sensor_type==SENSOR_TYPE_NODE_QUALITY \
|
|
2158
|
+
and self.__node_quality_data_raw is not None:
|
|
2159
|
+
data.append(self.__node_quality_data_raw)
|
|
2160
|
+
elif sensor_type==SENSOR_TYPE_NODE_DEMAND \
|
|
2161
|
+
and self.__demand_data_raw is not None:
|
|
2162
|
+
data.append(self.__demand_data_raw)
|
|
2163
|
+
elif sensor_type==SENSOR_TYPE_LINK_FLOW \
|
|
2164
|
+
and self.__flow_data_raw is not None:
|
|
2165
|
+
data.append(self.__flow_data_raw)
|
|
2166
|
+
elif sensor_type==SENSOR_TYPE_LINK_QUALITY \
|
|
2167
|
+
and self.__link_quality_data_raw is not None:
|
|
2168
|
+
data.append(self.__link_quality_data_raw)
|
|
2169
|
+
elif sensor_type==SENSOR_TYPE_VALVE_STATE \
|
|
2170
|
+
and self.__valves_state_data_raw is not None:
|
|
2171
|
+
data.append(self.__valves_state_data_raw)
|
|
2172
|
+
elif sensor_type==SENSOR_TYPE_PUMP_STATE \
|
|
2173
|
+
and self.__pumps_state_data_raw is not None:
|
|
2174
|
+
data.append(self.__pumps_state_data_raw)
|
|
2175
|
+
elif sensor_type==SENSOR_TYPE_TANK_VOLUME \
|
|
2176
|
+
and self.__tanks_volume_data_raw is not None:
|
|
2177
|
+
data.append(self.__tanks_volume_data_raw)
|
|
2178
|
+
elif sensor_type==SENSOR_TYPE_NODE_BULK_SPECIES \
|
|
2179
|
+
and self.__bulk_species_node_concentration_raw is not None:
|
|
2180
|
+
data.append(self.__bulk_species_node_concentration_raw)
|
|
2181
|
+
elif sensor_type==SENSOR_TYPE_LINK_BULK_SPECIES \
|
|
2182
|
+
and self.__bulk_species_link_concentration_raw is not None:
|
|
2183
|
+
data.append(self.__bulk_species_link_concentration_raw)
|
|
2184
|
+
elif sensor_type==SENSOR_TYPE_SURFACE_SPECIES \
|
|
2185
|
+
and self.__surface_species_concentration_raw is not None:
|
|
2186
|
+
data.append(self.__surface_species_concentration_raw)
|
|
2187
|
+
elif sensor_type==SENSOR_TYPE_PUMP_EFFICIENCY \
|
|
2188
|
+
and self.__pumps_efficiency_data_raw is not None:
|
|
2189
|
+
data.append(self.__pumps_efficiency_data_raw)
|
|
2190
|
+
elif sensor_type==SENSOR_TYPE_PUMP_ENERGYCONSUMPTION \
|
|
2191
|
+
and self.__pumps_energy_usage_data_raw is not None:
|
|
2192
|
+
data.append(self.__pumps_energy_usage_data_raw)
|
|
2193
|
+
elif sensor_type not in range(1,14):
|
|
2194
|
+
raise ValueError(
|
|
2195
|
+
f"Unknown sensor type '{sensor_type}' found in "
|
|
2196
|
+
f"'sensor_ordering'. Valid sensor types are:\n"
|
|
2197
|
+
f"{valid_sensor_types()}"
|
|
2198
|
+
)
|
|
2171
2199
|
sensor_readings = np.concatenate(data, axis=1)
|
|
2172
2200
|
|
|
2173
2201
|
# Apply sensor uncertainties
|
|
@@ -93,11 +93,11 @@ class SimpleControlModule(JsonSerializable):
|
|
|
93
93
|
if cond_comp_value < 0:
|
|
94
94
|
raise ValueError("'cond_comp_value' can not be negative")
|
|
95
95
|
|
|
96
|
-
self.
|
|
97
|
-
self.
|
|
98
|
-
self.
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
96
|
+
self._link_id = link_id
|
|
97
|
+
self._link_status = link_status
|
|
98
|
+
self._cond_type = cond_type
|
|
99
|
+
self._cond_var_value = cond_var_value
|
|
100
|
+
self._cond_comp_value = cond_comp_value
|
|
101
101
|
|
|
102
102
|
super().__init__(**kwds)
|
|
103
103
|
|
|
@@ -111,7 +111,7 @@ class SimpleControlModule(JsonSerializable):
|
|
|
111
111
|
`str`
|
|
112
112
|
Link ID.
|
|
113
113
|
"""
|
|
114
|
-
return self.
|
|
114
|
+
return self._link_id
|
|
115
115
|
|
|
116
116
|
@property
|
|
117
117
|
def link_status(self) -> Union[int, float]:
|
|
@@ -130,7 +130,7 @@ class SimpleControlModule(JsonSerializable):
|
|
|
130
130
|
- EN_SET_OPEN = 1e10
|
|
131
131
|
|
|
132
132
|
"""
|
|
133
|
-
return self.
|
|
133
|
+
return self._link_status
|
|
134
134
|
|
|
135
135
|
@property
|
|
136
136
|
def cond_type(self) -> int:
|
|
@@ -147,7 +147,7 @@ class SimpleControlModule(JsonSerializable):
|
|
|
147
147
|
- EN_TIMER = 2
|
|
148
148
|
- EN_TIMEOFDAY = 3
|
|
149
149
|
"""
|
|
150
|
-
return self.
|
|
150
|
+
return self._cond_type
|
|
151
151
|
|
|
152
152
|
@property
|
|
153
153
|
def cond_var_value(self) -> Union[str, int]:
|
|
@@ -163,7 +163,7 @@ class SimpleControlModule(JsonSerializable):
|
|
|
163
163
|
`str` or `int`
|
|
164
164
|
Condition/rule variable value.
|
|
165
165
|
"""
|
|
166
|
-
return self.
|
|
166
|
+
return self._cond_var_value
|
|
167
167
|
|
|
168
168
|
@property
|
|
169
169
|
def cond_comp_value(self) -> float:
|
|
@@ -178,37 +178,37 @@ class SimpleControlModule(JsonSerializable):
|
|
|
178
178
|
`float`
|
|
179
179
|
Condition/Rule comparison value.
|
|
180
180
|
"""
|
|
181
|
-
return self.
|
|
181
|
+
return self._cond_comp_value
|
|
182
182
|
|
|
183
183
|
def get_attributes(self) -> dict:
|
|
184
|
-
return super().get_attributes() | {"link_id": self.
|
|
185
|
-
"link_status": self.
|
|
186
|
-
"cond_type": self.
|
|
187
|
-
"cond_var_value": self.
|
|
188
|
-
"cond_comp_value": self.
|
|
184
|
+
return super().get_attributes() | {"link_id": self._link_id,
|
|
185
|
+
"link_status": self._link_status,
|
|
186
|
+
"cond_type": self._cond_type,
|
|
187
|
+
"cond_var_value": self._cond_var_value,
|
|
188
|
+
"cond_comp_value": self._cond_comp_value}
|
|
189
189
|
|
|
190
190
|
def __eq__(self, other) -> bool:
|
|
191
|
-
return super().__eq__(other) and self.
|
|
192
|
-
self.
|
|
193
|
-
self.
|
|
194
|
-
self.
|
|
191
|
+
return super().__eq__(other) and self._link_id == other.link_id and \
|
|
192
|
+
self._link_status == other.link_status and self._cond_type == other.cond_type and \
|
|
193
|
+
self._cond_var_value == other.cond_var_value and \
|
|
194
|
+
self._cond_comp_value == other.cond_comp_value
|
|
195
195
|
|
|
196
196
|
def __str__(self) -> str:
|
|
197
|
-
control_rule_str = f"LINK {self.
|
|
198
|
-
if isinstance(self.
|
|
199
|
-
control_rule_str += "OPEN " if self.
|
|
200
|
-
self.
|
|
197
|
+
control_rule_str = f"LINK {self._link_id} "
|
|
198
|
+
if isinstance(self._link_status, int):
|
|
199
|
+
control_rule_str += "OPEN " if self._link_status == ActuatorConstants.EN_OPEN or \
|
|
200
|
+
self._link_status == ActuatorConstants.EN_SET_OPEN else "CLOSED "
|
|
201
201
|
else:
|
|
202
|
-
control_rule_str += f"{self.
|
|
203
|
-
|
|
204
|
-
if self.
|
|
205
|
-
control_rule_str += f"AT TIME {self.
|
|
206
|
-
elif self.
|
|
207
|
-
control_rule_str += f"AT CLOCKTIME {self.
|
|
208
|
-
elif self.
|
|
209
|
-
control_rule_str += f"IF NODE {self.
|
|
210
|
-
elif self.
|
|
211
|
-
control_rule_str += f"IF NODE {self.
|
|
202
|
+
control_rule_str += f"{self._link_status} "
|
|
203
|
+
|
|
204
|
+
if self._cond_type == EpanetConstants.EN_TIMER:
|
|
205
|
+
control_rule_str += f"AT TIME {self._cond_var_value}"
|
|
206
|
+
elif self._cond_type == EpanetConstants.EN_TIMEOFDAY:
|
|
207
|
+
control_rule_str += f"AT CLOCKTIME {self._cond_var_value}"
|
|
208
|
+
elif self._cond_type == EpanetConstants.EN_LOWLEVEL:
|
|
209
|
+
control_rule_str += f"IF NODE {self._cond_var_value} BELOW {self._cond_comp_value}"
|
|
210
|
+
elif self._cond_type == EpanetConstants.EN_HILEVEL:
|
|
211
|
+
control_rule_str += f"IF NODE {self._cond_var_value} ABOVE {self._cond_comp_value}"
|
|
212
212
|
|
|
213
213
|
return control_rule_str
|
|
214
214
|
|
|
@@ -18,6 +18,7 @@ from .events.sensor_faults import SensorFaultConstant, SensorFaultDrift, SensorF
|
|
|
18
18
|
SensorFaultPercentage, SensorFaultStuckZero
|
|
19
19
|
from .events.leakages import AbruptLeakage, IncipientLeakage
|
|
20
20
|
from ..serialization import serializable, Serializable, SCENARIO_CONFIG_ID
|
|
21
|
+
from ..topology import NetworkTopology
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@serializable(SCENARIO_CONFIG_ID, ".epytflow_scenario_config")
|
|
@@ -46,6 +47,11 @@ class ScenarioConfig(Serializable):
|
|
|
46
47
|
Path to the .msx file -- optional, only necessary if EPANET-MSX is used.
|
|
47
48
|
|
|
48
49
|
The default is None
|
|
50
|
+
network_topology : :class:`~epyt_flow.topology.NetworkTopology`, optional
|
|
51
|
+
Specification of the network topology -- necessary if .inp file does not exist
|
|
52
|
+
or is not shared.
|
|
53
|
+
|
|
54
|
+
The default is None.
|
|
49
55
|
general_params : `dict`, optional
|
|
50
56
|
General parameters such as the demand model, hydraulic time steps, etc.
|
|
51
57
|
|
|
@@ -88,6 +94,7 @@ class ScenarioConfig(Serializable):
|
|
|
88
94
|
"""
|
|
89
95
|
|
|
90
96
|
def __init__(self, scenario_config: Any = None, f_inp_in: str = None, f_msx_in: str = None,
|
|
97
|
+
network_topology: NetworkTopology = None,
|
|
91
98
|
general_params: dict = None, sensor_config: SensorConfig = None,
|
|
92
99
|
memory_consumption_estimate: float = None,
|
|
93
100
|
custom_controls: list[CustomControlModule] = [],
|
|
@@ -112,6 +119,10 @@ class ScenarioConfig(Serializable):
|
|
|
112
119
|
if not isinstance(f_msx_in, str):
|
|
113
120
|
raise TypeError("'f_msx_in' must be an instance of 'str' " +
|
|
114
121
|
f"but no of '{type(f_msx_in)}'")
|
|
122
|
+
if network_topology is not None:
|
|
123
|
+
if not isinstance(network_topology, NetworkTopology):
|
|
124
|
+
raise TypeError("'network_topology' msut be an instance of 'NetworkTopology' " +
|
|
125
|
+
f"but not of '{type(network_topology)}'")
|
|
115
126
|
if general_params is not None:
|
|
116
127
|
if not isinstance(general_params, dict):
|
|
117
128
|
raise TypeError("'general_params' must be an instance of 'dict' " +
|
|
@@ -172,6 +183,11 @@ class ScenarioConfig(Serializable):
|
|
|
172
183
|
self.__f_inp_in = scenario_config.f_inp_in
|
|
173
184
|
self.__f_msx_in = scenario_config.f_msx_in if f_msx_in is None else f_msx_in
|
|
174
185
|
|
|
186
|
+
if network_topology is None:
|
|
187
|
+
self.__network_topology = scenario_config.network_topology
|
|
188
|
+
else:
|
|
189
|
+
self.__network_topology = network_topology
|
|
190
|
+
|
|
175
191
|
if general_params is None:
|
|
176
192
|
self.__general_params = scenario_config.general_params
|
|
177
193
|
else:
|
|
@@ -224,6 +240,7 @@ class ScenarioConfig(Serializable):
|
|
|
224
240
|
else:
|
|
225
241
|
self.__f_inp_in = f_inp_in
|
|
226
242
|
self.__f_msx_in = f_msx_in
|
|
243
|
+
self.__network_topology = network_topology
|
|
227
244
|
self.__general_params = general_params
|
|
228
245
|
self.__sensor_config = sensor_config
|
|
229
246
|
self.__memory_consumption_estimate = memory_consumption_estimate
|
|
@@ -278,6 +295,18 @@ class ScenarioConfig(Serializable):
|
|
|
278
295
|
else:
|
|
279
296
|
return self.__f_msx_in
|
|
280
297
|
|
|
298
|
+
@property
|
|
299
|
+
def network_topology(self) -> NetworkTopology:
|
|
300
|
+
"""
|
|
301
|
+
Returns the specification of the network topology.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
:class:`~epyt_flow.topology.NetworkTopology`
|
|
306
|
+
Network topology.
|
|
307
|
+
"""
|
|
308
|
+
return deepcopy(self.__network_topology)
|
|
309
|
+
|
|
281
310
|
@property
|
|
282
311
|
def general_params(self) -> dict:
|
|
283
312
|
"""
|
|
@@ -402,6 +431,7 @@ class ScenarioConfig(Serializable):
|
|
|
402
431
|
|
|
403
432
|
def get_attributes(self) -> dict:
|
|
404
433
|
my_attributes = {"f_inp_in": self.__f_inp_in, "f_msx_in": self.__f_msx_in,
|
|
434
|
+
"network_topology": self.__network_topology,
|
|
405
435
|
"general_params": self.__general_params,
|
|
406
436
|
"sensor_config": self.__sensor_config,
|
|
407
437
|
"memory_consumption_estimate": self.__memory_consumption_estimate,
|
|
@@ -421,6 +451,7 @@ class ScenarioConfig(Serializable):
|
|
|
421
451
|
f"with '{type(other)}' instance")
|
|
422
452
|
|
|
423
453
|
return self.__f_inp_in == other.f_inp_in and self.__f_msx_in == other.f_msx_in \
|
|
454
|
+
and self.__network_topology == other.network_topology \
|
|
424
455
|
and self.__general_params == other.general_params \
|
|
425
456
|
and self.__memory_consumption_estimate == other.memory_consumption_estimate \
|
|
426
457
|
and self.__sensor_config == other.sensor_config \
|