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
|
@@ -5,6 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
import itertools
|
|
6
6
|
import numpy as np
|
|
7
7
|
from epanet_plus import EpanetConstants, EPyT
|
|
8
|
+
from typing import Optional
|
|
8
9
|
|
|
9
10
|
from ..serialization import SENSOR_CONFIG_ID, JsonSerializable, serializable
|
|
10
11
|
|
|
@@ -34,6 +35,25 @@ TIME_UNIT_HRS = 8
|
|
|
34
35
|
MASS_UNIT_CUSTOM = 9
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def valid_sensor_types() -> str:
|
|
39
|
+
"""Returns a description of all valid sensor types."""
|
|
40
|
+
return (
|
|
41
|
+
f"{SENSOR_TYPE_NODE_PRESSURE} (Node Pressure), "
|
|
42
|
+
f"{SENSOR_TYPE_NODE_QUALITY} (Node Quality), "
|
|
43
|
+
f"{SENSOR_TYPE_NODE_DEMAND} (Node Demand), "
|
|
44
|
+
f"{SENSOR_TYPE_LINK_FLOW} (Link Flow), "
|
|
45
|
+
f"{SENSOR_TYPE_LINK_QUALITY} (Link Quality), "
|
|
46
|
+
f"{SENSOR_TYPE_VALVE_STATE} (Valve State), "
|
|
47
|
+
f"{SENSOR_TYPE_PUMP_STATE} (Pump State), "
|
|
48
|
+
f"{SENSOR_TYPE_TANK_VOLUME} (Tank Volume), "
|
|
49
|
+
f"{SENSOR_TYPE_NODE_BULK_SPECIES} (Node Bulk Species), "
|
|
50
|
+
f"{SENSOR_TYPE_LINK_BULK_SPECIES} (Link Bulk Species), "
|
|
51
|
+
f"{SENSOR_TYPE_SURFACE_SPECIES} (Surface Species), "
|
|
52
|
+
f"{SENSOR_TYPE_PUMP_EFFICIENCY} (Pump Efficiency), "
|
|
53
|
+
f"{SENSOR_TYPE_PUMP_ENERGYCONSUMPTION} (Pump Energy Consumption)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
37
57
|
def areaunit_to_id(unit_desc: str) -> int:
|
|
38
58
|
"""
|
|
39
59
|
Converts a given area units string to the corresponding ID.
|
|
@@ -754,6 +774,21 @@ class SensorConfig(JsonSerializable):
|
|
|
754
774
|
self.__bulk_species_mass_unit = bulk_species_mass_unit
|
|
755
775
|
self.__surface_species_mass_unit = surface_species_mass_unit
|
|
756
776
|
self.__surface_species_area_unit = surface_species_area_unit
|
|
777
|
+
self.__sensor_ordering = [
|
|
778
|
+
SENSOR_TYPE_NODE_PRESSURE,
|
|
779
|
+
SENSOR_TYPE_LINK_FLOW,
|
|
780
|
+
SENSOR_TYPE_NODE_DEMAND,
|
|
781
|
+
SENSOR_TYPE_NODE_QUALITY,
|
|
782
|
+
SENSOR_TYPE_LINK_QUALITY,
|
|
783
|
+
SENSOR_TYPE_VALVE_STATE,
|
|
784
|
+
SENSOR_TYPE_PUMP_STATE,
|
|
785
|
+
SENSOR_TYPE_PUMP_EFFICIENCY,
|
|
786
|
+
SENSOR_TYPE_PUMP_ENERGYCONSUMPTION,
|
|
787
|
+
SENSOR_TYPE_TANK_VOLUME,
|
|
788
|
+
SENSOR_TYPE_SURFACE_SPECIES,
|
|
789
|
+
SENSOR_TYPE_NODE_BULK_SPECIES,
|
|
790
|
+
SENSOR_TYPE_LINK_BULK_SPECIES,
|
|
791
|
+
]
|
|
757
792
|
|
|
758
793
|
self.__compute_indices() # Compute indices
|
|
759
794
|
|
|
@@ -952,6 +987,29 @@ class SensorConfig(JsonSerializable):
|
|
|
952
987
|
"""
|
|
953
988
|
return self.__surfacespecies_id_to_idx
|
|
954
989
|
|
|
990
|
+
@property
|
|
991
|
+
def sensor_ordering(self) -> list[int]:
|
|
992
|
+
"""
|
|
993
|
+
Returns the order in which sensors are included in ScadaData objects
|
|
994
|
+
i.e. if you call a ScadaData's get_data() method, the resulting array
|
|
995
|
+
will contain sensor readings in the order returned by this method.
|
|
996
|
+
Constants have the following meaning:
|
|
997
|
+
1 -> pressure sensor
|
|
998
|
+
2 -> node quality sensor
|
|
999
|
+
3 -> demand sensor
|
|
1000
|
+
4 -> flow sensor
|
|
1001
|
+
5 -> link quality sensor
|
|
1002
|
+
6 -> valve state sensor
|
|
1003
|
+
7 -> pump state sensor
|
|
1004
|
+
8 -> tank volume sensor
|
|
1005
|
+
9 -> node bulk species sensor
|
|
1006
|
+
10 -> link bulk species sensor
|
|
1007
|
+
11 -> surface species sensor
|
|
1008
|
+
12 -> pump efficiency sensor
|
|
1009
|
+
13 -> pump energy consumption sensor
|
|
1010
|
+
"""
|
|
1011
|
+
return self.__sensor_ordering
|
|
1012
|
+
|
|
955
1013
|
def map_node_id_to_idx(self, node_id: str) -> int:
|
|
956
1014
|
"""
|
|
957
1015
|
Gets the index of a given node ID.
|
|
@@ -1139,20 +1197,55 @@ class SensorConfig(JsonSerializable):
|
|
|
1139
1197
|
*self.__bulk_species_node_sensors.values())))
|
|
1140
1198
|
n_bulk_species_link_sensors = len(list(itertools.chain(
|
|
1141
1199
|
*self.__bulk_species_link_sensors.values())))
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1200
|
+
n_surface_species_sensors = len(list(itertools.chain(
|
|
1201
|
+
*self.__surface_species_sensors.values())))
|
|
1202
|
+
|
|
1203
|
+
current_shift = 0
|
|
1204
|
+
for sensor_type in self.__sensor_ordering:
|
|
1205
|
+
if sensor_type == SENSOR_TYPE_NODE_PRESSURE:
|
|
1206
|
+
pressure_idx_shift = current_shift
|
|
1207
|
+
current_shift += n_pressure_sensors
|
|
1208
|
+
elif sensor_type == SENSOR_TYPE_LINK_FLOW:
|
|
1209
|
+
flow_idx_shift = current_shift
|
|
1210
|
+
current_shift += n_flow_sensors
|
|
1211
|
+
elif sensor_type == SENSOR_TYPE_NODE_QUALITY:
|
|
1212
|
+
node_quality_idx_shift = current_shift
|
|
1213
|
+
current_shift += n_node_quality_sensors
|
|
1214
|
+
elif sensor_type == SENSOR_TYPE_NODE_DEMAND:
|
|
1215
|
+
demand_idx_shift = current_shift
|
|
1216
|
+
current_shift += n_demand_sensors
|
|
1217
|
+
elif sensor_type == SENSOR_TYPE_LINK_QUALITY:
|
|
1218
|
+
link_quality_idx_shift = current_shift
|
|
1219
|
+
current_shift += n_link_quality_sensors
|
|
1220
|
+
elif sensor_type == SENSOR_TYPE_VALVE_STATE:
|
|
1221
|
+
valve_state_idx_shift = current_shift
|
|
1222
|
+
current_shift += n_valve_state_sensors
|
|
1223
|
+
elif sensor_type == SENSOR_TYPE_PUMP_STATE:
|
|
1224
|
+
pump_state_idx_shift = current_shift
|
|
1225
|
+
current_shift += n_pump_state_sensors
|
|
1226
|
+
elif sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY:
|
|
1227
|
+
pump_efficiency_idx_shift = current_shift
|
|
1228
|
+
current_shift += n_pump_efficiency_sensors
|
|
1229
|
+
elif sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
|
|
1230
|
+
pump_energyconsumption_idx_shift = current_shift
|
|
1231
|
+
current_shift += n_pump_energyconsumption_sensors
|
|
1232
|
+
elif sensor_type == SENSOR_TYPE_TANK_VOLUME:
|
|
1233
|
+
tank_volume_idx_shift = current_shift
|
|
1234
|
+
current_shift += n_tank_volume_sensors
|
|
1235
|
+
elif sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
|
|
1236
|
+
bulk_species_node_idx_shift = current_shift
|
|
1237
|
+
current_shift += n_bulk_species_node_sensors
|
|
1238
|
+
elif sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
|
|
1239
|
+
bulk_species_link_idx_shift = current_shift
|
|
1240
|
+
current_shift += n_bulk_species_link_sensors
|
|
1241
|
+
elif sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
|
|
1242
|
+
surface_species_idx_shift = current_shift
|
|
1243
|
+
current_shift += n_surface_species_sensors
|
|
1244
|
+
else:
|
|
1245
|
+
raise ValueError(
|
|
1246
|
+
f"Invalid sensor type: {sensor_type}. "
|
|
1247
|
+
f"Valid sensor types are:\n{valid_sensor_types()}"
|
|
1248
|
+
)
|
|
1156
1249
|
|
|
1157
1250
|
def __build_sensors_id_to_idx(sensors: list[str], initial_idx_shift: int) -> dict:
|
|
1158
1251
|
return {sensor_id: i + initial_idx_shift
|
|
@@ -1983,6 +2076,17 @@ class SensorConfig(JsonSerializable):
|
|
|
1983
2076
|
return self.__surface_species_mass_unit[self.map_surfacespecies_id_to_idx(
|
|
1984
2077
|
surface_species_id)]
|
|
1985
2078
|
|
|
2079
|
+
def _append_readings_if_possible(self, data: list, reading: Optional[np.ndarray], reading_idx: list, request_condition: bool, sensor_description: str) -> list:
|
|
2080
|
+
if reading is not None:
|
|
2081
|
+
data.append(reading[:, reading_idx])
|
|
2082
|
+
else:
|
|
2083
|
+
if request_condition:
|
|
2084
|
+
raise ValueError(
|
|
2085
|
+
f"{sensor_description} readings requested, "
|
|
2086
|
+
f"but no such data is given"
|
|
2087
|
+
)
|
|
2088
|
+
return data
|
|
2089
|
+
|
|
1986
2090
|
def compute_readings(self, pressures: np.ndarray, flows: np.ndarray, demands: np.ndarray,
|
|
1987
2091
|
nodes_quality: np.ndarray, links_quality: np.ndarray,
|
|
1988
2092
|
pumps_state: np.ndarray, pumps_efficiency: np.ndarray,
|
|
@@ -2058,100 +2162,108 @@ class SensorConfig(JsonSerializable):
|
|
|
2058
2162
|
"""
|
|
2059
2163
|
data = []
|
|
2060
2164
|
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
reshape(-1, len(
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2165
|
+
for sensor_type in self.sensor_ordering:
|
|
2166
|
+
if sensor_type==SENSOR_TYPE_NODE_PRESSURE:
|
|
2167
|
+
data = self._append_readings_if_possible(
|
|
2168
|
+
data, pressures, self.__pressure_idx,
|
|
2169
|
+
len(self.__pressure_sensors) != 0,
|
|
2170
|
+
"Pressure"
|
|
2171
|
+
)
|
|
2172
|
+
elif sensor_type==SENSOR_TYPE_NODE_QUALITY:
|
|
2173
|
+
data = self._append_readings_if_possible(
|
|
2174
|
+
data, nodes_quality, self.__quality_node_idx,
|
|
2175
|
+
len(self.__quality_node_sensors) != 0,
|
|
2176
|
+
"Node water quality"
|
|
2177
|
+
)
|
|
2178
|
+
elif sensor_type==SENSOR_TYPE_NODE_DEMAND:
|
|
2179
|
+
data = self._append_readings_if_possible(
|
|
2180
|
+
data, demands, self.__demand_idx,
|
|
2181
|
+
len(self.__demand_sensors) != 0,
|
|
2182
|
+
"Demand"
|
|
2183
|
+
)
|
|
2184
|
+
elif sensor_type==SENSOR_TYPE_LINK_FLOW:
|
|
2185
|
+
data = self._append_readings_if_possible(
|
|
2186
|
+
data, flows, self.__flow_idx,
|
|
2187
|
+
len(self.__flow_sensors) != 0,
|
|
2188
|
+
"Flow"
|
|
2189
|
+
)
|
|
2190
|
+
elif sensor_type==SENSOR_TYPE_LINK_QUALITY:
|
|
2191
|
+
data = self._append_readings_if_possible(
|
|
2192
|
+
data, links_quality, self.__quality_link_idx,
|
|
2193
|
+
len(self.__quality_link_sensors) != 0,
|
|
2194
|
+
"Link/Pipe water quality"
|
|
2195
|
+
)
|
|
2196
|
+
elif sensor_type==SENSOR_TYPE_VALVE_STATE:
|
|
2197
|
+
data = self._append_readings_if_possible(
|
|
2198
|
+
data, valves_state, self.__valve_state_idx,
|
|
2199
|
+
len(self.__valve_state_sensors) != 0,
|
|
2200
|
+
"Valve state"
|
|
2201
|
+
)
|
|
2202
|
+
elif sensor_type==SENSOR_TYPE_PUMP_STATE:
|
|
2203
|
+
data = self._append_readings_if_possible(
|
|
2204
|
+
data, pumps_state, self.__pump_state_idx,
|
|
2205
|
+
len(self.__pump_state_sensors) != 0,
|
|
2206
|
+
"Pump state"
|
|
2207
|
+
)
|
|
2208
|
+
elif sensor_type==SENSOR_TYPE_TANK_VOLUME:
|
|
2209
|
+
data = self._append_readings_if_possible(
|
|
2210
|
+
data, tanks_volume, self.__tank_volume_idx,
|
|
2211
|
+
len(self.__tank_volume_sensors) != 0,
|
|
2212
|
+
"Tank water volume"
|
|
2213
|
+
)
|
|
2214
|
+
elif sensor_type==SENSOR_TYPE_NODE_BULK_SPECIES:
|
|
2215
|
+
if bulk_species_node_concentrations is not None:
|
|
2216
|
+
for species_idx, nodes_idx in self.__bulk_species_node_idx:
|
|
2217
|
+
data.append(
|
|
2218
|
+
bulk_species_node_concentrations[
|
|
2219
|
+
:, species_idx, nodes_idx
|
|
2220
|
+
].reshape(-1, len(nodes_idx))
|
|
2221
|
+
)
|
|
2222
|
+
else:
|
|
2223
|
+
if len(self.__bulk_species_node_sensors) != 0:
|
|
2224
|
+
raise ValueError("Bulk species concentratinons requested but no " +
|
|
2225
|
+
"bulk species node concentration data is given")
|
|
2226
|
+
elif sensor_type==SENSOR_TYPE_LINK_BULK_SPECIES:
|
|
2227
|
+
if bulk_species_link_concentrations is not None:
|
|
2228
|
+
for species_idx, links_idx in self.__bulk_species_link_idx:
|
|
2229
|
+
data.append(
|
|
2230
|
+
bulk_species_link_concentrations[
|
|
2231
|
+
:, species_idx, links_idx
|
|
2232
|
+
].reshape(-1, len(links_idx))
|
|
2233
|
+
)
|
|
2234
|
+
else:
|
|
2235
|
+
if len(self.__bulk_species_link_sensors) != 0:
|
|
2236
|
+
raise ValueError("Bulk species concentratinons requested but no " +
|
|
2237
|
+
"bulk species link/pipe concentration data is given")
|
|
2238
|
+
elif sensor_type==SENSOR_TYPE_SURFACE_SPECIES:
|
|
2239
|
+
if surface_species_concentrations is not None:
|
|
2240
|
+
for species_idx, links_idx in self.__surface_species_idx:
|
|
2241
|
+
data.append(
|
|
2242
|
+
surface_species_concentrations[
|
|
2243
|
+
:, species_idx, links_idx
|
|
2244
|
+
].reshape(-1, len(links_idx))
|
|
2245
|
+
)
|
|
2246
|
+
else:
|
|
2247
|
+
if len(self.__surface_species_sensors) != 0:
|
|
2248
|
+
raise ValueError("Surface species concentratinons requested but no " +
|
|
2249
|
+
"surface species concentration data is given")
|
|
2250
|
+
elif sensor_type==SENSOR_TYPE_PUMP_EFFICIENCY:
|
|
2251
|
+
data = self._append_readings_if_possible(
|
|
2252
|
+
data, pumps_efficiency, self.__pump_efficiency_idx,
|
|
2253
|
+
len(self.__pump_efficiency_sensors) != 0,
|
|
2254
|
+
"Pump efficiency"
|
|
2255
|
+
)
|
|
2256
|
+
elif sensor_type==SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
|
|
2257
|
+
data = self._append_readings_if_possible(
|
|
2258
|
+
data, pumps_energyconsumption, self.__pump_energyconsumption_idx,
|
|
2259
|
+
len(self.__pump_energyconsumption_sensors) != 0,
|
|
2260
|
+
"Pump energy consumption"
|
|
2261
|
+
)
|
|
2262
|
+
else:
|
|
2263
|
+
raise ValueError(
|
|
2264
|
+
f"Unknown sensor type '{sensor_type}'. "
|
|
2265
|
+
f"Valid sensor types are\n{valid_sensor_types()}"
|
|
2266
|
+
)
|
|
2155
2267
|
return np.concatenate(data, axis=1)
|
|
2156
2268
|
|
|
2157
2269
|
def get_index_of_reading(self, pressure_sensor: str = None, flow_sensor: str = None,
|