epyt-flow 0.10.0__py3-none-any.whl → 0.12.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/gecco_water_quality.py +2 -2
- epyt_flow/data/benchmarks/leakdb.py +40 -5
- epyt_flow/data/benchmarks/water_usage.py +4 -3
- epyt_flow/data/networks.py +27 -14
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +11 -13
- epyt_flow/rest_api/scenario/control_handlers.py +118 -0
- epyt_flow/rest_api/scenario/event_handlers.py +114 -1
- epyt_flow/rest_api/scenario/handlers.py +33 -0
- epyt_flow/rest_api/server.py +14 -2
- epyt_flow/serialization.py +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/backend/__init__.py +1 -0
- epyt_flow/simulation/backend/my_epyt.py +1056 -0
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/quality_events.py +3 -1
- epyt_flow/simulation/scada/scada_data.py +716 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +645 -119
- epyt_flow/simulation/sensor_config.py +18 -2
- epyt_flow/topology.py +24 -7
- epyt_flow/uncertainty/model_uncertainty.py +80 -62
- epyt_flow/uncertainty/sensor_noise.py +15 -4
- epyt_flow/uncertainty/uncertainties.py +71 -18
- epyt_flow/uncertainty/utils.py +40 -13
- epyt_flow/utils.py +45 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/visualization/scenario_visualizer.py +1240 -0
- epyt_flow/visualization/visualization_utils.py +738 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/METADATA +15 -4
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +35 -36
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/WHEEL +1 -1
- epyt_flow/gym/control_gyms.py +0 -47
- epyt_flow/metrics.py +0 -466
- epyt_flow/models/__init__.py +0 -2
- epyt_flow/models/event_detector.py +0 -31
- epyt_flow/models/sensor_interpolation_detector.py +0 -118
- epyt_flow/simulation/scada/advanced_control.py +0 -138
- epyt_flow/simulation/scenario_visualizer.py +0 -1307
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -22,6 +22,7 @@ from ..sensor_config import SensorConfig, is_flowunit_simetric, massunit_to_str,
|
|
|
22
22
|
from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
|
|
23
23
|
from ...uncertainty import SensorNoise
|
|
24
24
|
from ...serialization import serializable, Serializable, SCADA_DATA_ID
|
|
25
|
+
from ...topology import NetworkTopology
|
|
25
26
|
from ...utils import plot_timeseries_data
|
|
26
27
|
|
|
27
28
|
|
|
@@ -40,6 +41,10 @@ class ScadaData(Serializable):
|
|
|
40
41
|
|
|
41
42
|
This parameter is expected to be a 1d array with the same size as
|
|
42
43
|
the number of rows in `sensor_readings_data_raw`.
|
|
44
|
+
network_topo : :class:`~epyt_flow.topology.NetworkTopology`
|
|
45
|
+
Topology of the water distribution network.
|
|
46
|
+
warnings_code : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
47
|
+
Codes/IDs of EPANET errors/warnings (if any) for each time step.
|
|
43
48
|
pressure_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
44
49
|
Raw pressure values of all nodes as a two-dimensional array --
|
|
45
50
|
first dimension encodes time, second dimension pressure at nodes.
|
|
@@ -129,6 +134,7 @@ class ScadaData(Serializable):
|
|
|
129
134
|
The default is False.
|
|
130
135
|
"""
|
|
131
136
|
def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
|
|
137
|
+
network_topo: NetworkTopology, warnings_code: np.ndarray = None,
|
|
132
138
|
pressure_data_raw: Union[np.ndarray, bsr_array] = None,
|
|
133
139
|
flow_data_raw: Union[np.ndarray, bsr_array] = None,
|
|
134
140
|
demand_data_raw: Union[np.ndarray, bsr_array] = None,
|
|
@@ -149,6 +155,10 @@ class ScadaData(Serializable):
|
|
|
149
155
|
sensor_reading_events: list[SensorReadingEvent] = [],
|
|
150
156
|
sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
|
|
151
157
|
**kwds):
|
|
158
|
+
if not isinstance(network_topo, NetworkTopology):
|
|
159
|
+
raise TypeError("'network_topo' must be an instance of " +
|
|
160
|
+
"'epyt_flow.topology.NetworkTopology' but not " +
|
|
161
|
+
f"of '{type(network_topo)}'")
|
|
152
162
|
if not isinstance(sensor_config, SensorConfig):
|
|
153
163
|
raise TypeError("'sensor_config' must be an instance of " +
|
|
154
164
|
"'epyt_flow.simulation.SensorConfig' but not of " +
|
|
@@ -156,6 +166,14 @@ class ScadaData(Serializable):
|
|
|
156
166
|
if not isinstance(sensor_readings_time, np.ndarray):
|
|
157
167
|
raise TypeError("'sensor_readings_time' must be an instance of 'numpy.ndarray' " +
|
|
158
168
|
f"but not of '{type(sensor_readings_time)}'")
|
|
169
|
+
if warnings_code is None:
|
|
170
|
+
warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
|
|
171
|
+
" -- support of such old files will be removed in the next release!",
|
|
172
|
+
DeprecationWarning)
|
|
173
|
+
else:
|
|
174
|
+
if not isinstance(warnings_code, np.ndarray):
|
|
175
|
+
raise TypeError("'warnings_code' must be an instance of 'numpy.ndarray' " +
|
|
176
|
+
f"but not of '{type(warnings_code)}'")
|
|
159
177
|
if pressure_data_raw is not None:
|
|
160
178
|
if not isinstance(pressure_data_raw, np.ndarray) and \
|
|
161
179
|
not isinstance(pressure_data_raw, bsr_array):
|
|
@@ -293,6 +311,9 @@ class ScadaData(Serializable):
|
|
|
293
311
|
"must match number of raw measurements.")
|
|
294
312
|
|
|
295
313
|
n_time_steps = sensor_readings_time.shape[0]
|
|
314
|
+
if warnings_code is not None:
|
|
315
|
+
if warnings_code.shape[0] != n_time_steps:
|
|
316
|
+
__raise_shape_mismatch("warnings_code")
|
|
296
317
|
if pressure_data_raw is not None:
|
|
297
318
|
if pressure_data_raw.shape[0] != n_time_steps:
|
|
298
319
|
__raise_shape_mismatch("pressure_data_raw")
|
|
@@ -345,7 +366,9 @@ class ScadaData(Serializable):
|
|
|
345
366
|
if pumps_efficiency_data_raw.shape[0] != n_time_steps:
|
|
346
367
|
__raise_shape_mismatch("pumps_efficiency_data_raw")
|
|
347
368
|
|
|
369
|
+
self.__network_topo = network_topo
|
|
348
370
|
self.__sensor_config = sensor_config
|
|
371
|
+
self.__warnings_code = warnings_code
|
|
349
372
|
self.__sensor_noise = sensor_noise
|
|
350
373
|
self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
|
|
351
374
|
sensor_reading_events
|
|
@@ -1030,7 +1053,9 @@ class ScadaData(Serializable):
|
|
|
1030
1053
|
surface_species_mass_unit=new_surface_species_mass_unit,
|
|
1031
1054
|
surface_species_area_unit=new_surface_species_area_unit)
|
|
1032
1055
|
|
|
1033
|
-
return ScadaData(
|
|
1056
|
+
return ScadaData(network_topo=self.network_topo,
|
|
1057
|
+
warnings_code=self.warnings_code,
|
|
1058
|
+
sensor_config=sensor_config,
|
|
1034
1059
|
sensor_readings_time=self.sensor_readings_time,
|
|
1035
1060
|
sensor_reading_events=self.sensor_reading_events,
|
|
1036
1061
|
sensor_noise=self.sensor_noise,
|
|
@@ -1049,6 +1074,31 @@ class ScadaData(Serializable):
|
|
|
1049
1074
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1050
1075
|
surface_species_concentration_raw=surface_species_concentrations)
|
|
1051
1076
|
|
|
1077
|
+
@property
|
|
1078
|
+
def network_topo(self) -> NetworkTopology:
|
|
1079
|
+
"""
|
|
1080
|
+
Returns the topology of the water distribution network.
|
|
1081
|
+
|
|
1082
|
+
Returns
|
|
1083
|
+
-------
|
|
1084
|
+
:class:`epyt_flow.topology.NetworkTopology`
|
|
1085
|
+
Topology of the network.
|
|
1086
|
+
"""
|
|
1087
|
+
return deepcopy(self.__network_topo)
|
|
1088
|
+
|
|
1089
|
+
@property
|
|
1090
|
+
def warnings_code(self) -> np.ndarray:
|
|
1091
|
+
"""
|
|
1092
|
+
Returns the codes/IDs of EPANET errors/warnings (if any) for each time step.
|
|
1093
|
+
Note that zero denotes the absence of any error/warning.
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
--------
|
|
1097
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1098
|
+
Codes/IDs of EPANET errors/warnings (if any) for each time step.
|
|
1099
|
+
"""
|
|
1100
|
+
return deepcopy(self.__warnings_code)
|
|
1101
|
+
|
|
1052
1102
|
@property
|
|
1053
1103
|
def frozen_sensor_config(self) -> bool:
|
|
1054
1104
|
"""
|
|
@@ -1374,7 +1424,9 @@ class ScadaData(Serializable):
|
|
|
1374
1424
|
self.__sensor_readings = None
|
|
1375
1425
|
|
|
1376
1426
|
def get_attributes(self) -> dict:
|
|
1377
|
-
attr = {"
|
|
1427
|
+
attr = {"network_topo": self.__network_topo,
|
|
1428
|
+
"warnings_code": self.__warnings_code,
|
|
1429
|
+
"sensor_config": self.__sensor_config,
|
|
1378
1430
|
"frozen_sensor_config": self.__frozen_sensor_config,
|
|
1379
1431
|
"sensor_noise": self.__sensor_noise,
|
|
1380
1432
|
"sensor_reading_events": self.__sensor_reading_events,
|
|
@@ -1525,7 +1577,9 @@ class ScadaData(Serializable):
|
|
|
1525
1577
|
raise TypeError(f"Can not compare 'ScadaData' instance to '{type(other)}' instance")
|
|
1526
1578
|
|
|
1527
1579
|
try:
|
|
1528
|
-
return self.
|
|
1580
|
+
return self.__network_topo == other.network_topo \
|
|
1581
|
+
and np.all(self.__warnings_code == other.warnings_code) \
|
|
1582
|
+
and self.__sensor_config == other.sensor_config \
|
|
1529
1583
|
and self.__frozen_sensor_config == other.frozen_sensor_config \
|
|
1530
1584
|
and self.__sensor_noise == other.sensor_noise \
|
|
1531
1585
|
and all(a == b for a, b in
|
|
@@ -1553,7 +1607,8 @@ class ScadaData(Serializable):
|
|
|
1553
1607
|
return False
|
|
1554
1608
|
|
|
1555
1609
|
def __str__(self) -> str:
|
|
1556
|
-
return f"sensor_config: {self.__sensor_config} " + \
|
|
1610
|
+
return f"network_topo: {self.__network_topo} sensor_config: {self.__sensor_config} " + \
|
|
1611
|
+
f"warnings_code: {self.__warnings_code} " + \
|
|
1557
1612
|
f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
|
|
1558
1613
|
f"sensor_noise: {self.__sensor_noise} " + \
|
|
1559
1614
|
f"sensor_reading_events: {self.__sensor_reading_events} " + \
|
|
@@ -1751,8 +1806,9 @@ class ScadaData(Serializable):
|
|
|
1751
1806
|
if self.__pumps_efficiency_data_raw is not None:
|
|
1752
1807
|
pumps_efficiency_data_raw = self.__pumps_efficiency_data_raw[start_idx:end_idx, :]
|
|
1753
1808
|
|
|
1754
|
-
return ScadaData(sensor_config=self.sensor_config,
|
|
1809
|
+
return ScadaData(network_topo=self.network_topo, sensor_config=self.sensor_config,
|
|
1755
1810
|
sensor_readings_time=self.sensor_readings_time[start_idx:end_idx],
|
|
1811
|
+
warnings_code=self.__warnings_code[start_idx:end_idx],
|
|
1756
1812
|
frozen_sensor_config=self.frozen_sensor_config,
|
|
1757
1813
|
sensor_noise=self.sensor_noise,
|
|
1758
1814
|
sensor_reading_events=self.sensor_reading_events,
|
|
@@ -1789,6 +1845,8 @@ class ScadaData(Serializable):
|
|
|
1789
1845
|
if not isinstance(other, ScadaData):
|
|
1790
1846
|
raise TypeError("'other' must be an instance of 'ScadaData' " +
|
|
1791
1847
|
f"but not of '{type(other)}'")
|
|
1848
|
+
if self.__network_topo != other.network_topo:
|
|
1849
|
+
raise ValueError("Network topology must be the same in both instances")
|
|
1792
1850
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
1793
1851
|
raise ValueError("Sensor configurations of both instances must be " +
|
|
1794
1852
|
"either frozen or not frozen")
|
|
@@ -1891,6 +1949,8 @@ class ScadaData(Serializable):
|
|
|
1891
1949
|
"""
|
|
1892
1950
|
if not isinstance(other, ScadaData):
|
|
1893
1951
|
raise TypeError(f"'other' must be an instance of 'ScadaData' but not of {type(other)}")
|
|
1952
|
+
if self.__network_topo != other.network_topo:
|
|
1953
|
+
raise ValueError("Network topology must be the same")
|
|
1894
1954
|
if self.__sensor_config != other.sensor_config:
|
|
1895
1955
|
raise ValueError("Sensor configurations must be the same!")
|
|
1896
1956
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
@@ -1907,6 +1967,8 @@ class ScadaData(Serializable):
|
|
|
1907
1967
|
self.__sensor_readings_time = np.concatenate(
|
|
1908
1968
|
(self.__sensor_readings_time, other.sensor_readings_time), axis=0)
|
|
1909
1969
|
|
|
1970
|
+
self.__warnings_code = np.concatenate((self.__warnings_code, other.warnings_code), axis=0)
|
|
1971
|
+
|
|
1910
1972
|
if self.__pressure_data_raw is not None:
|
|
1911
1973
|
self.__pressure_data_raw = np.concatenate(
|
|
1912
1974
|
(self.__pressure_data_raw, other.pressure_data_raw), axis=0)
|
|
@@ -1967,11 +2029,91 @@ class ScadaData(Serializable):
|
|
|
1967
2029
|
(self.__pumps_efficiency_data_raw, other.pumps_efficiency_data_raw),
|
|
1968
2030
|
axis=0)
|
|
1969
2031
|
|
|
2032
|
+
def topo_adj_matrix(self) -> bsr_array:
|
|
2033
|
+
"""
|
|
2034
|
+
Returns the adjacency matrix of the network.
|
|
2035
|
+
|
|
2036
|
+
Nodes are ordered according to EPANET.
|
|
2037
|
+
|
|
2038
|
+
Returns
|
|
2039
|
+
-------
|
|
2040
|
+
`scipy.bsr_array <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.bsr_array.html>`_
|
|
2041
|
+
Adjacency matrix as a sparse array of shape [num_nodes, num_nodes].
|
|
2042
|
+
"""
|
|
2043
|
+
return self.__network_topo.get_adj_matrix()
|
|
2044
|
+
|
|
2045
|
+
def map_link_id_to_edge_idx(self, link_id: str) -> tuple[int, int]:
|
|
2046
|
+
"""
|
|
2047
|
+
Maps a given link to the corresponding two indices in the edge indices as computed by
|
|
2048
|
+
:func:`epyt_flow.simulation.scada.scada_data.ScadaData.topo_edge_indices`.
|
|
2049
|
+
|
|
2050
|
+
Returns
|
|
2051
|
+
-------
|
|
2052
|
+
`tuple[int, int]`
|
|
2053
|
+
Indices.
|
|
2054
|
+
"""
|
|
2055
|
+
if not isinstance(link_id, str):
|
|
2056
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
2057
|
+
if link_id not in self.__sensor_config.links:
|
|
2058
|
+
raise ValueError(f"Unknown link '{link_id}'")
|
|
2059
|
+
|
|
2060
|
+
idx = 0
|
|
2061
|
+
for l_id, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2062
|
+
if l_id == link_id:
|
|
2063
|
+
return (idx, idx+1)
|
|
2064
|
+
|
|
2065
|
+
idx += 2
|
|
2066
|
+
|
|
2067
|
+
def get_topo_edge_indices(self) -> np.ndarray:
|
|
2068
|
+
"""
|
|
2069
|
+
Returns the edge indices -- i.e. a 2 dimensional array where the first dimension denotes
|
|
2070
|
+
the source node indices and the second dimension denotes the target node indices
|
|
2071
|
+
for all links in the network.
|
|
2072
|
+
Nodes are ordered according to EPANET.
|
|
2073
|
+
|
|
2074
|
+
Note that the network is consideres as a directed graph -- i.e. one link corresponds to
|
|
2075
|
+
two edges in opposite directions!
|
|
2076
|
+
|
|
2077
|
+
Returns
|
|
2078
|
+
-------
|
|
2079
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2080
|
+
Edge indices of shape [2, num_links * 2].
|
|
2081
|
+
"""
|
|
2082
|
+
edge_indices = [[], []]
|
|
2083
|
+
|
|
2084
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2085
|
+
links = self.__network_topo.get_all_links()
|
|
2086
|
+
|
|
2087
|
+
for _, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2088
|
+
node_a_idx = nodes_id.index(node_a_id)
|
|
2089
|
+
node_b_idx = nodes_id.index(node_b_id)
|
|
2090
|
+
|
|
2091
|
+
edge_indices[0] += [node_a_idx, node_b_idx]
|
|
2092
|
+
edge_indices[1] += [node_b_idx, node_a_idx]
|
|
2093
|
+
|
|
2094
|
+
return np.array(edge_indices)
|
|
2095
|
+
|
|
1970
2096
|
def get_data(self) -> np.ndarray:
|
|
1971
2097
|
"""
|
|
1972
2098
|
Computes the final sensor readings -- note that those might be subject to
|
|
1973
2099
|
given sensor faults and sensor noise/uncertainty.
|
|
1974
2100
|
|
|
2101
|
+
Columns (i.e. sensor readings) are ordered as follows:
|
|
2102
|
+
|
|
2103
|
+
1. Pressures
|
|
2104
|
+
2. Flows
|
|
2105
|
+
3. Demands
|
|
2106
|
+
4. Nodes quality
|
|
2107
|
+
5. Links quality
|
|
2108
|
+
6. Valve state
|
|
2109
|
+
7. Pumps state
|
|
2110
|
+
8. Pumps efficiency
|
|
2111
|
+
9. Pumps energy consumption
|
|
2112
|
+
10. Tanks volume
|
|
2113
|
+
11. Surface species concentrations
|
|
2114
|
+
12. Bulk species nodes concentrations
|
|
2115
|
+
13. Bulk species links concentrations
|
|
2116
|
+
|
|
1975
2117
|
Returns
|
|
1976
2118
|
-------
|
|
1977
2119
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
@@ -2050,6 +2192,116 @@ class ScadaData(Serializable):
|
|
|
2050
2192
|
|
|
2051
2193
|
return sensor_readings
|
|
2052
2194
|
|
|
2195
|
+
def get_data_node_features(self, default_missing_value: float = 0.
|
|
2196
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2197
|
+
"""
|
|
2198
|
+
Returns the sensor readings as node features together with a boolean mask indicating the
|
|
2199
|
+
presence of a sensor -- i.e. pressure, demand, quality, bulk species concentration
|
|
2200
|
+
at each node.
|
|
2201
|
+
|
|
2202
|
+
Note that only quantities with at least one sensor are considered.
|
|
2203
|
+
|
|
2204
|
+
Parameters
|
|
2205
|
+
----------
|
|
2206
|
+
default_missing_value : `float`, optional
|
|
2207
|
+
Default value (i.e. missing value) for nodes where no sensor is installed.
|
|
2208
|
+
|
|
2209
|
+
The default is 0.
|
|
2210
|
+
|
|
2211
|
+
Returns
|
|
2212
|
+
-------
|
|
2213
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2214
|
+
Node features of shape [num_nodes, num_time_steps, num_node_features], and
|
|
2215
|
+
mask of shape [num_nodes, num_node_features].
|
|
2216
|
+
"""
|
|
2217
|
+
node_features = []
|
|
2218
|
+
node_features_mask = []
|
|
2219
|
+
|
|
2220
|
+
if len(self.__sensor_config.pressure_sensors) != 0:
|
|
2221
|
+
features, node_mask = self.get_data_pressures_as_node_features(default_missing_value)
|
|
2222
|
+
features = features.T
|
|
2223
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2224
|
+
node_features.append(features)
|
|
2225
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2226
|
+
|
|
2227
|
+
if len(self.__sensor_config.demand_sensors) != 0:
|
|
2228
|
+
features, node_mask = self.get_data_demands_as_node_features(default_missing_value)
|
|
2229
|
+
features = features.T
|
|
2230
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2231
|
+
node_features.append(features)
|
|
2232
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2233
|
+
|
|
2234
|
+
if len(self.__sensor_config.quality_node_sensors) != 0:
|
|
2235
|
+
features, node_mask = self.get_data_nodes_quality_as_node_features(default_missing_value)
|
|
2236
|
+
features = features.T
|
|
2237
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2238
|
+
node_features.append(features)
|
|
2239
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2240
|
+
|
|
2241
|
+
if len(self.__sensor_config.bulk_species_node_sensors) != 0:
|
|
2242
|
+
features, node_mask = self.\
|
|
2243
|
+
get_data_bulk_species_concentrations_as_node_features(default_missing_value)
|
|
2244
|
+
features = np.swapaxes(features, 0, 1)
|
|
2245
|
+
node_features.append(features)
|
|
2246
|
+
node_features_mask.append(node_mask)
|
|
2247
|
+
|
|
2248
|
+
return np.concatenate(node_features, axis=2), np.concatenate(node_features_mask, axis=1)
|
|
2249
|
+
|
|
2250
|
+
def get_data_edge_features(self, default_missing_value: float = 0.
|
|
2251
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2252
|
+
"""
|
|
2253
|
+
Returns the sensor readings as edge features together with a boolean mask indicating the
|
|
2254
|
+
presence of a sensor -- i.e. flow, quality, surface species concentration,
|
|
2255
|
+
bulk species concentration at each link.
|
|
2256
|
+
|
|
2257
|
+
Note that only quantities with at least one sensor are considered.
|
|
2258
|
+
|
|
2259
|
+
Parameters
|
|
2260
|
+
----------
|
|
2261
|
+
default_missing_value : `float`, optional
|
|
2262
|
+
Default value (i.e. missing value) for links where no sensor is installed.
|
|
2263
|
+
|
|
2264
|
+
The default is 0.
|
|
2265
|
+
|
|
2266
|
+
Returns
|
|
2267
|
+
-------
|
|
2268
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2269
|
+
Edge features of shape [num_links, num_time_steps, num_edge_features] and
|
|
2270
|
+
mask of shape [num_links, num_edge_features].
|
|
2271
|
+
"""
|
|
2272
|
+
edge_features = []
|
|
2273
|
+
edge_features_mask = []
|
|
2274
|
+
|
|
2275
|
+
if len(self.__sensor_config.flow_sensors) != 0:
|
|
2276
|
+
features, link_mask = self.get_data_flows_as_edge_features(default_missing_value)
|
|
2277
|
+
features = features.T
|
|
2278
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2279
|
+
edge_features.append(features)
|
|
2280
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2281
|
+
|
|
2282
|
+
if len(self.__sensor_config.quality_link_sensors) != 0:
|
|
2283
|
+
features, link_mask = self.get_data_links_quality_as_edge_features(default_missing_value)
|
|
2284
|
+
features = features.T
|
|
2285
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2286
|
+
edge_features.append(features)
|
|
2287
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2288
|
+
|
|
2289
|
+
if len(self.__sensor_config.surface_species_sensors) != 0:
|
|
2290
|
+
features, link_mask = self.\
|
|
2291
|
+
get_data_surface_species_concentrations_as_edge_features(default_missing_value)
|
|
2292
|
+
features = np.swapaxes(features, 0, 1)
|
|
2293
|
+
edge_features.append(features)
|
|
2294
|
+
edge_features_mask.append(link_mask)
|
|
2295
|
+
|
|
2296
|
+
if len(self.__sensor_config.bulk_species_link_sensors) != 0:
|
|
2297
|
+
features, link_mask = self.\
|
|
2298
|
+
get_data_bulk_species_concentrations_as_edge_features(default_missing_value)
|
|
2299
|
+
features = np.swapaxes(features, 0, 1)
|
|
2300
|
+
edge_features.append(features)
|
|
2301
|
+
edge_features_mask.append(link_mask)
|
|
2302
|
+
|
|
2303
|
+
return np.concatenate(edge_features, axis=2), np.concatenate(edge_features_mask, axis=1)
|
|
2304
|
+
|
|
2053
2305
|
def __get_x_axis_label(self) -> str:
|
|
2054
2306
|
if len(self.__sensor_readings_time) > 1:
|
|
2055
2307
|
time_step = self.__sensor_readings_time[1] - self.__sensor_readings_time[0]
|
|
@@ -2101,6 +2353,38 @@ class ScadaData(Serializable):
|
|
|
2101
2353
|
for s_id in sensor_locations]
|
|
2102
2354
|
return self.__sensor_readings[:, idx]
|
|
2103
2355
|
|
|
2356
|
+
def get_data_pressures_as_node_features(self,
|
|
2357
|
+
default_missing_value: float = 0.
|
|
2358
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2359
|
+
"""
|
|
2360
|
+
Returns the pressures as node features together with a boolean mask indicating the
|
|
2361
|
+
presence of a sensor.
|
|
2362
|
+
|
|
2363
|
+
Parameters
|
|
2364
|
+
----------
|
|
2365
|
+
default_missing_value : `float`, optional
|
|
2366
|
+
Default value (i.e. missing value) for nodes where no pressure sensor is installed.
|
|
2367
|
+
|
|
2368
|
+
The default is 0.
|
|
2369
|
+
|
|
2370
|
+
Returns
|
|
2371
|
+
-------
|
|
2372
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2373
|
+
Pressures as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2374
|
+
"""
|
|
2375
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2376
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2377
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2378
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2379
|
+
|
|
2380
|
+
pressure_readings = self.get_data_pressures()
|
|
2381
|
+
for pressures_idx, node_id in enumerate(self.__sensor_config.pressure_sensors):
|
|
2382
|
+
idx = nodes_id.index(node_id)
|
|
2383
|
+
node_features[:, idx] = pressure_readings[:, pressures_idx]
|
|
2384
|
+
mask[idx] = 1
|
|
2385
|
+
|
|
2386
|
+
return node_features, mask
|
|
2387
|
+
|
|
2104
2388
|
def plot_pressures(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2105
2389
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2106
2390
|
) -> matplotlib.axes.Axes:
|
|
@@ -2188,6 +2472,43 @@ class ScadaData(Serializable):
|
|
|
2188
2472
|
for s_id in sensor_locations]
|
|
2189
2473
|
return self.__sensor_readings[:, idx]
|
|
2190
2474
|
|
|
2475
|
+
def get_data_flows_as_edge_features(self, default_missing_value: float = 0.
|
|
2476
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2477
|
+
"""
|
|
2478
|
+
Returns the flows as edge features together with a boolean mask indicating the
|
|
2479
|
+
presence of a sensor.
|
|
2480
|
+
|
|
2481
|
+
Note that the second link has the opposite flow direction of the flow at the first link --
|
|
2482
|
+
recall that we have an undirected graph, i.e. two edges per link.
|
|
2483
|
+
|
|
2484
|
+
Parameters
|
|
2485
|
+
----------
|
|
2486
|
+
default_missing_value : `float`, optional
|
|
2487
|
+
Default value (i.e. missing value) for links where no flow sensor is installed.
|
|
2488
|
+
|
|
2489
|
+
The default is 0.
|
|
2490
|
+
|
|
2491
|
+
Returns
|
|
2492
|
+
-------
|
|
2493
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2494
|
+
Flows as edge features of shape [num_time_steps, num_links * 2] and mask of shape [num_links * 2].
|
|
2495
|
+
"""
|
|
2496
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2497
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2498
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2499
|
+
|
|
2500
|
+
flow_readings = self.get_data_flows()
|
|
2501
|
+
for flows_idx, link_id in enumerate(self.__sensor_config.flow_sensors):
|
|
2502
|
+
idx1, idx2 = self.map_link_id_to_edge_idx(link_id)
|
|
2503
|
+
|
|
2504
|
+
mask[idx1] = 1
|
|
2505
|
+
mask[idx2] = 1
|
|
2506
|
+
|
|
2507
|
+
edge_features[:, idx1] = flow_readings[:, flows_idx]
|
|
2508
|
+
edge_features[:, idx2] = -1 * flow_readings[:, flows_idx]
|
|
2509
|
+
|
|
2510
|
+
return edge_features, mask
|
|
2511
|
+
|
|
2191
2512
|
def plot_flows(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2192
2513
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2193
2514
|
) -> matplotlib.axes.Axes:
|
|
@@ -2274,6 +2595,38 @@ class ScadaData(Serializable):
|
|
|
2274
2595
|
for s_id in sensor_locations]
|
|
2275
2596
|
return self.__sensor_readings[:, idx]
|
|
2276
2597
|
|
|
2598
|
+
def get_data_demands_as_node_features(self,
|
|
2599
|
+
default_missing_value: float = 0.
|
|
2600
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2601
|
+
"""
|
|
2602
|
+
Returns the demands as node features together with a boolean mask indicating the
|
|
2603
|
+
presence of a sensor.
|
|
2604
|
+
|
|
2605
|
+
Parameters
|
|
2606
|
+
----------
|
|
2607
|
+
default_missing_value : `float`, optional
|
|
2608
|
+
Default value (i.e. missing value) for nodes where no demand sensor is installed.
|
|
2609
|
+
|
|
2610
|
+
The default is 0.
|
|
2611
|
+
|
|
2612
|
+
Returns
|
|
2613
|
+
-------
|
|
2614
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2615
|
+
Demands as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2616
|
+
"""
|
|
2617
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2618
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2619
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2620
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2621
|
+
|
|
2622
|
+
demand_readings = self.get_data_demands()
|
|
2623
|
+
for demands_idx, node_id in enumerate(self.__sensor_config.demand_sensors):
|
|
2624
|
+
idx = nodes_id.index(node_id)
|
|
2625
|
+
node_features[:, idx] = demand_readings[:, demands_idx]
|
|
2626
|
+
mask[idx] = 1
|
|
2627
|
+
|
|
2628
|
+
return node_features, mask
|
|
2629
|
+
|
|
2277
2630
|
def plot_demands(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2278
2631
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2279
2632
|
) -> matplotlib.axes.Axes:
|
|
@@ -2361,6 +2714,39 @@ class ScadaData(Serializable):
|
|
|
2361
2714
|
for s_id in sensor_locations]
|
|
2362
2715
|
return self.__sensor_readings[:, idx]
|
|
2363
2716
|
|
|
2717
|
+
def get_data_nodes_quality_as_node_features(self,
|
|
2718
|
+
default_missing_value: float = 0
|
|
2719
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2720
|
+
"""
|
|
2721
|
+
Returns the nodes' quality as node features together with a boolean mask indicating the
|
|
2722
|
+
presence of a sensor.
|
|
2723
|
+
|
|
2724
|
+
Parameters
|
|
2725
|
+
----------
|
|
2726
|
+
default_missing_value : `float`, optional
|
|
2727
|
+
Default value (i.e. missing value) for nodes where no quality sensor is installed.
|
|
2728
|
+
|
|
2729
|
+
The default is 0.
|
|
2730
|
+
|
|
2731
|
+
Returns
|
|
2732
|
+
-------
|
|
2733
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2734
|
+
Nodes' quality as node features of shape [num_time_steps, num_nodes], and mask of
|
|
2735
|
+
shape [num_nodes].
|
|
2736
|
+
"""
|
|
2737
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2738
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2739
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2740
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2741
|
+
|
|
2742
|
+
node_quality_readings = self.get_data_nodes_quality()
|
|
2743
|
+
for quality_idx, node_id in enumerate(self.__sensor_config.quality_node_sensors):
|
|
2744
|
+
idx = nodes_id.index(node_id)
|
|
2745
|
+
node_features[:, idx] = node_quality_readings[:, quality_idx]
|
|
2746
|
+
mask[idx] = 1
|
|
2747
|
+
|
|
2748
|
+
return node_features, mask
|
|
2749
|
+
|
|
2364
2750
|
def plot_nodes_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2365
2751
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2366
2752
|
) -> matplotlib.axes.Axes:
|
|
@@ -2450,6 +2836,38 @@ class ScadaData(Serializable):
|
|
|
2450
2836
|
for s_id in sensor_locations]
|
|
2451
2837
|
return self.__sensor_readings[:, idx]
|
|
2452
2838
|
|
|
2839
|
+
def get_data_links_quality_as_edge_features(self,
|
|
2840
|
+
default_missing_value: float = 0
|
|
2841
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2842
|
+
"""
|
|
2843
|
+
Returns the links' quality as edge features together with a boolean mask indicating the
|
|
2844
|
+
presence of a sensor.
|
|
2845
|
+
|
|
2846
|
+
Parameters
|
|
2847
|
+
----------
|
|
2848
|
+
default_missing_value : `float`, optional
|
|
2849
|
+
Default value (i.e. missing value) for links where no quality sensor is installed.
|
|
2850
|
+
|
|
2851
|
+
The default is 0.
|
|
2852
|
+
|
|
2853
|
+
Returns
|
|
2854
|
+
-------
|
|
2855
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2856
|
+
Links' quality as edge features of shape [num_time_steps, num_links * 2], and mask of
|
|
2857
|
+
shape [num_links * 2].
|
|
2858
|
+
"""
|
|
2859
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2860
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2861
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2862
|
+
|
|
2863
|
+
links_quality_readings = self.get_data_links_quality()
|
|
2864
|
+
for quality_idx, link_id in enumerate(self.__sensor_config.quality_link_sensors):
|
|
2865
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
2866
|
+
edge_features[:, idx] = links_quality_readings[:, quality_idx]
|
|
2867
|
+
mask[idx] = 1
|
|
2868
|
+
|
|
2869
|
+
return edge_features, mask
|
|
2870
|
+
|
|
2453
2871
|
def plot_links_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2454
2872
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2455
2873
|
) -> matplotlib.axes.Axes:
|
|
@@ -2539,6 +2957,38 @@ class ScadaData(Serializable):
|
|
|
2539
2957
|
for s_id in sensor_locations]
|
|
2540
2958
|
return self.__sensor_readings[:, idx]
|
|
2541
2959
|
|
|
2960
|
+
def get_data_pumps_state_as_node_features(self,
|
|
2961
|
+
default_missing_value: float = 0.
|
|
2962
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2963
|
+
"""
|
|
2964
|
+
Returns the pump state as node features together with a boolean mask indicating the
|
|
2965
|
+
presence of a sensor.
|
|
2966
|
+
|
|
2967
|
+
Parameters
|
|
2968
|
+
----------
|
|
2969
|
+
default_missing_value : `float`, optional
|
|
2970
|
+
Default value (i.e. missing value) for nodes where no pump state sensor is installed.
|
|
2971
|
+
|
|
2972
|
+
The default is 0.
|
|
2973
|
+
|
|
2974
|
+
Returns
|
|
2975
|
+
-------
|
|
2976
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2977
|
+
Pump state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2978
|
+
"""
|
|
2979
|
+
mask = np.zeros(len(self.__sensor_config.pumps))
|
|
2980
|
+
pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
|
|
2981
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2982
|
+
pumps_id = self.__network_topo.get_all_pumps()
|
|
2983
|
+
|
|
2984
|
+
state_readings = self.get_data_pumps_state()
|
|
2985
|
+
for pumps_state_idx, pump_id in enumerate(self.__sensor_config.pump_state_sensors):
|
|
2986
|
+
idx = pumps_id.index(pump_id)
|
|
2987
|
+
pump_features[:, idx] = state_readings[:, pumps_state_idx]
|
|
2988
|
+
mask[idx] = 1
|
|
2989
|
+
|
|
2990
|
+
return pump_features, mask
|
|
2991
|
+
|
|
2542
2992
|
def plot_pumps_state(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2543
2993
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2544
2994
|
) -> matplotlib.axes.Axes:
|
|
@@ -2626,6 +3076,38 @@ class ScadaData(Serializable):
|
|
|
2626
3076
|
for s_id in sensor_locations]
|
|
2627
3077
|
return self.__sensor_readings[:, idx]
|
|
2628
3078
|
|
|
3079
|
+
def get_data_pumps_efficiency_as_node_features(self,
|
|
3080
|
+
default_missing_value: float = 0.
|
|
3081
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3082
|
+
"""
|
|
3083
|
+
Returns the pump efficiency as node features together with a boolean mask indicating the
|
|
3084
|
+
presence of a sensor.
|
|
3085
|
+
|
|
3086
|
+
Parameters
|
|
3087
|
+
----------
|
|
3088
|
+
default_missing_value : `float`, optional
|
|
3089
|
+
Default value (i.e. missing value) for nodes where no pump efficiency sensor is installed.
|
|
3090
|
+
|
|
3091
|
+
The default is 0.
|
|
3092
|
+
|
|
3093
|
+
Returns
|
|
3094
|
+
-------
|
|
3095
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3096
|
+
Pump efficiencies as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
3097
|
+
"""
|
|
3098
|
+
mask = np.zeros(len(self.__sensor_config.pumps))
|
|
3099
|
+
pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
|
|
3100
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3101
|
+
pumps_id = self.__network_topo.get_all_pumps()
|
|
3102
|
+
|
|
3103
|
+
efficiency_readings = self.get_data_pumps_efficiency()
|
|
3104
|
+
for pumps_efficiency_idx, pump_id in enumerate(self.__sensor_config.pump_efficiency_sensors):
|
|
3105
|
+
idx = pumps_id.index(pump_id)
|
|
3106
|
+
pump_features[:, idx] = efficiency_readings[:, pumps_efficiency_idx]
|
|
3107
|
+
mask[idx] = 1
|
|
3108
|
+
|
|
3109
|
+
return pump_features, mask
|
|
3110
|
+
|
|
2629
3111
|
def plot_pumps_efficiency(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2630
3112
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2631
3113
|
) -> matplotlib.axes.Axes:
|
|
@@ -2714,6 +3196,38 @@ class ScadaData(Serializable):
|
|
|
2714
3196
|
for s_id in sensor_locations]
|
|
2715
3197
|
return self.__sensor_readings[:, idx]
|
|
2716
3198
|
|
|
3199
|
+
def get_data_pumps_energyconsumption_as_node_features(self,
|
|
3200
|
+
default_missing_value: float = 0.
|
|
3201
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3202
|
+
"""
|
|
3203
|
+
Returns the pump energy consumption as node features together with a boolean mask indicating the
|
|
3204
|
+
presence of a sensor.
|
|
3205
|
+
|
|
3206
|
+
Parameters
|
|
3207
|
+
----------
|
|
3208
|
+
default_missing_value : `float`, optional
|
|
3209
|
+
Default value (i.e. missing value) for nodes where no pump energy consumption sensor is installed.
|
|
3210
|
+
|
|
3211
|
+
The default is 0.
|
|
3212
|
+
|
|
3213
|
+
Returns
|
|
3214
|
+
-------
|
|
3215
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3216
|
+
Pump energy consumptions as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
3217
|
+
"""
|
|
3218
|
+
mask = np.zeros(len(self.__sensor_config.pumps))
|
|
3219
|
+
pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
|
|
3220
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3221
|
+
pumps_id = self.__network_topo.get_all_pumps()
|
|
3222
|
+
|
|
3223
|
+
energyconsumption_readings = self.get_data_pumps_energyconsumption()
|
|
3224
|
+
for pumps_energyconsumption_idx, pump_id in enumerate(self.__sensor_config.pump_energyconsumption_sensors):
|
|
3225
|
+
idx = pumps_id.index(pump_id)
|
|
3226
|
+
pump_features[:, idx] = energyconsumption_readings[:, pumps_energyconsumption_idx]
|
|
3227
|
+
mask[idx] = 1
|
|
3228
|
+
|
|
3229
|
+
return pump_features, mask
|
|
3230
|
+
|
|
2717
3231
|
def plot_pumps_energyconsumption(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2718
3232
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2719
3233
|
) -> matplotlib.axes.Axes:
|
|
@@ -2801,6 +3315,38 @@ class ScadaData(Serializable):
|
|
|
2801
3315
|
for s_id in sensor_locations]
|
|
2802
3316
|
return self.__sensor_readings[:, idx]
|
|
2803
3317
|
|
|
3318
|
+
def get_data_valves_state_as_node_features(self,
|
|
3319
|
+
default_missing_value: float = 0.
|
|
3320
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3321
|
+
"""
|
|
3322
|
+
Returns the valves state as node features together with a boolean mask indicating the
|
|
3323
|
+
presence of a sensor.
|
|
3324
|
+
|
|
3325
|
+
Parameters
|
|
3326
|
+
----------
|
|
3327
|
+
default_missing_value : `float`, optional
|
|
3328
|
+
Default value (i.e. missing value) for nodes where no valves state sensor is installed.
|
|
3329
|
+
|
|
3330
|
+
The default is 0.
|
|
3331
|
+
|
|
3332
|
+
Returns
|
|
3333
|
+
-------
|
|
3334
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3335
|
+
Valves state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
3336
|
+
"""
|
|
3337
|
+
mask = np.zeros(len(self.__sensor_config.valves))
|
|
3338
|
+
valve_features = np.array([[default_missing_value] * len(self.__sensor_config.valves)
|
|
3339
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3340
|
+
valves_id = self.__network_topo.get_all_valves()
|
|
3341
|
+
|
|
3342
|
+
state_readings = self.get_data_valves_state()
|
|
3343
|
+
for valves_state_idx, valve_id in enumerate(self.__sensor_config.valve_state_sensors):
|
|
3344
|
+
idx = valves_id.index(valve_id)
|
|
3345
|
+
valve_features[:, idx] = state_readings[:, valves_state_idx]
|
|
3346
|
+
mask[idx] = 1
|
|
3347
|
+
|
|
3348
|
+
return valve_features, mask
|
|
3349
|
+
|
|
2804
3350
|
def plot_valves_state(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2805
3351
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2806
3352
|
) -> matplotlib.axes.Axes:
|
|
@@ -2888,6 +3434,38 @@ class ScadaData(Serializable):
|
|
|
2888
3434
|
for s_id in sensor_locations]
|
|
2889
3435
|
return self.__sensor_readings[:, idx]
|
|
2890
3436
|
|
|
3437
|
+
def get_data_tanks_water_volume_as_node_features(self,
|
|
3438
|
+
default_missing_value: float = 0.
|
|
3439
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3440
|
+
"""
|
|
3441
|
+
Returns the tank water volume as node features together with a boolean mask indicating the
|
|
3442
|
+
presence of a sensor.
|
|
3443
|
+
|
|
3444
|
+
Parameters
|
|
3445
|
+
----------
|
|
3446
|
+
default_missing_value : `float`, optional
|
|
3447
|
+
Default value (i.e. missing value) for nodes where no tank water volume sensor is installed.
|
|
3448
|
+
|
|
3449
|
+
The default is 0.
|
|
3450
|
+
|
|
3451
|
+
Returns
|
|
3452
|
+
-------
|
|
3453
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3454
|
+
Tank water volumes as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
3455
|
+
"""
|
|
3456
|
+
mask = np.zeros(len(self.__sensor_config.tanks))
|
|
3457
|
+
tank_features = np.array([[default_missing_value] * len(self.__sensor_config.tanks)
|
|
3458
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3459
|
+
tanks_id = self.__network_topo.get_all_tanks()
|
|
3460
|
+
|
|
3461
|
+
water_volume_readings = self.get_data_tanks_water_volume()
|
|
3462
|
+
for tanks_water_volume_idx, tank_id in enumerate(self.__sensor_config.tank_volume_sensors):
|
|
3463
|
+
idx = tanks_id.index(tank_id)
|
|
3464
|
+
tank_features[:, idx] = water_volume_readings[:, tanks_water_volume_idx]
|
|
3465
|
+
mask[idx] = 1
|
|
3466
|
+
|
|
3467
|
+
return tank_features, mask
|
|
3468
|
+
|
|
2891
3469
|
def plot_tanks_water_volume(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2892
3470
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2893
3471
|
) -> matplotlib.axes.Axes:
|
|
@@ -2988,6 +3566,50 @@ class ScadaData(Serializable):
|
|
|
2988
3566
|
for link_id in surface_species_sensor_locations[species_id]]
|
|
2989
3567
|
return self.__sensor_readings[:, idx]
|
|
2990
3568
|
|
|
3569
|
+
def get_data_surface_species_concentrations_as_edge_features(self,
|
|
3570
|
+
default_missing_value: float = 0.
|
|
3571
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3572
|
+
"""
|
|
3573
|
+
Returns the concentrations of surface species as edge features together with a
|
|
3574
|
+
boolean mask indicating the presence of a sensor.
|
|
3575
|
+
|
|
3576
|
+
Note that only surface species with at least one sensor are considered.
|
|
3577
|
+
|
|
3578
|
+
Parameters
|
|
3579
|
+
----------
|
|
3580
|
+
default_missing_value : `float`, optional
|
|
3581
|
+
Default value (i.e. missing value) for links where no surface species
|
|
3582
|
+
sensor is installed.
|
|
3583
|
+
|
|
3584
|
+
The default is 0.
|
|
3585
|
+
|
|
3586
|
+
Returns
|
|
3587
|
+
-------
|
|
3588
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3589
|
+
Concentrations of surface species as edge features of shape
|
|
3590
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3591
|
+
shape [num_links * 2, num_species].
|
|
3592
|
+
"""
|
|
3593
|
+
masks = []
|
|
3594
|
+
results = []
|
|
3595
|
+
|
|
3596
|
+
surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
|
|
3597
|
+
for species_id, links_id in surface_species_sensor_locations.items():
|
|
3598
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3599
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3600
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3601
|
+
|
|
3602
|
+
sensor_readings = self.get_data_surface_species_concentration({species_id: links_id})
|
|
3603
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3604
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3605
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3606
|
+
mask[idx] = 1
|
|
3607
|
+
|
|
3608
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3609
|
+
masks.append(mask.reshape(-1, 1))
|
|
3610
|
+
|
|
3611
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3612
|
+
|
|
2991
3613
|
def plot_surface_species_concentration(self, surface_species_sensor_locations: dict = None,
|
|
2992
3614
|
show: bool = True, save_to_file: str = None,
|
|
2993
3615
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3104,6 +3726,51 @@ class ScadaData(Serializable):
|
|
|
3104
3726
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3105
3727
|
return self.__sensor_readings[:, idx]
|
|
3106
3728
|
|
|
3729
|
+
def get_data_bulk_species_concentrations_as_node_features(self,
|
|
3730
|
+
default_missing_value: float = 0.
|
|
3731
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3732
|
+
"""
|
|
3733
|
+
Returns the concentrations of bulk species as node features together with a boolean mask
|
|
3734
|
+
indicating the presence of a sensor.
|
|
3735
|
+
|
|
3736
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3737
|
+
|
|
3738
|
+
Parameters
|
|
3739
|
+
----------
|
|
3740
|
+
default_missing_value : `float`, optional
|
|
3741
|
+
Default value (i.e. missing value) for nodes where no bulk species
|
|
3742
|
+
sensor is installed.
|
|
3743
|
+
|
|
3744
|
+
The default is 0.
|
|
3745
|
+
|
|
3746
|
+
Returns
|
|
3747
|
+
-------
|
|
3748
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3749
|
+
Concentrations of bulk species as node features of shape
|
|
3750
|
+
[num_time_steps, num_nodes, num_species], and mask of shape [num_nodes, num_species].
|
|
3751
|
+
"""
|
|
3752
|
+
masks = []
|
|
3753
|
+
results = []
|
|
3754
|
+
|
|
3755
|
+
all_nodes_id = self.__network_topo.get_all_nodes()
|
|
3756
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
|
|
3757
|
+
|
|
3758
|
+
for species_id, nodes_id in bulk_species_sensor_locations.items():
|
|
3759
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
3760
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
3761
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3762
|
+
|
|
3763
|
+
sensor_readings = self.get_data_bulk_species_node_concentration({species_id: nodes_id})
|
|
3764
|
+
for sensor_readings_idx, node_id in enumerate(nodes_id):
|
|
3765
|
+
idx = all_nodes_id.index(node_id)
|
|
3766
|
+
node_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3767
|
+
mask[idx] = 1
|
|
3768
|
+
|
|
3769
|
+
results.append(node_features.reshape(node_features.shape[0], node_features.shape[1],1))
|
|
3770
|
+
masks.append(mask.reshape(-1, 1))
|
|
3771
|
+
|
|
3772
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3773
|
+
|
|
3107
3774
|
def plot_bulk_species_node_concentration(self, bulk_species_node_sensors: dict = None,
|
|
3108
3775
|
show: bool = True, save_to_file: str = None,
|
|
3109
3776
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3219,6 +3886,50 @@ class ScadaData(Serializable):
|
|
|
3219
3886
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3220
3887
|
return self.__sensor_readings[:, idx]
|
|
3221
3888
|
|
|
3889
|
+
def get_data_bulk_species_concentrations_as_edge_features(self,
|
|
3890
|
+
default_missing_value: float = 0.
|
|
3891
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3892
|
+
"""
|
|
3893
|
+
Returns the concentrations of bulk species as edge features together with a boolean mask
|
|
3894
|
+
indicating the presence of a sensor.
|
|
3895
|
+
|
|
3896
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3897
|
+
|
|
3898
|
+
Parameters
|
|
3899
|
+
----------
|
|
3900
|
+
default_missing_value : `float`, optional
|
|
3901
|
+
Default value (i.e. missing value) for links where no bulk species
|
|
3902
|
+
sensor is installed.
|
|
3903
|
+
|
|
3904
|
+
The default is 0.
|
|
3905
|
+
|
|
3906
|
+
Returns
|
|
3907
|
+
-------
|
|
3908
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3909
|
+
Concentrations of bulk species as edge features of shape
|
|
3910
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3911
|
+
shape [num_links * 2, num_species].
|
|
3912
|
+
"""
|
|
3913
|
+
masks = []
|
|
3914
|
+
results = []
|
|
3915
|
+
|
|
3916
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
|
|
3917
|
+
for species_id, links_id in bulk_species_sensor_locations.items():
|
|
3918
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3919
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3920
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3921
|
+
|
|
3922
|
+
sensor_readings = self.get_data_bulk_species_link_concentration({species_id: links_id})
|
|
3923
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3924
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3925
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3926
|
+
mask[idx] = 1
|
|
3927
|
+
|
|
3928
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3929
|
+
masks.append(mask.reshape(-1, 1))
|
|
3930
|
+
|
|
3931
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3932
|
+
|
|
3222
3933
|
def plot_bulk_species_link_concentration(self, bulk_species_link_sensors: dict = None,
|
|
3223
3934
|
show: bool = True, save_to_file: str = None,
|
|
3224
3935
|
ax: matplotlib.axes.Axes = None
|