epyt-flow 0.10.0__py3-none-any.whl → 0.11.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/networks.py +27 -14
- epyt_flow/gym/control_gyms.py +8 -0
- epyt_flow/gym/scenario_control_env.py +8 -3
- epyt_flow/metrics.py +5 -0
- epyt_flow/models/event_detector.py +5 -0
- epyt_flow/models/sensor_interpolation_detector.py +5 -0
- epyt_flow/serialization.py +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/scada/scada_data.py +527 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +511 -68
- epyt_flow/simulation/sensor_config.py +18 -2
- epyt_flow/topology.py +16 -0
- 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 +15 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/{simulation → visualization}/scenario_visualizer.py +429 -586
- epyt_flow/visualization/visualization_utils.py +611 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/METADATA +12 -1
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/RECORD +28 -26
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.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
|
|
|
@@ -127,6 +128,8 @@ class ScadaData(Serializable):
|
|
|
127
128
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
128
129
|
|
|
129
130
|
The default is False.
|
|
131
|
+
network_topo : :class:`~epyt_flow.topology.NetworkTopology`
|
|
132
|
+
Topology of the water distribution network.
|
|
130
133
|
"""
|
|
131
134
|
def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
|
|
132
135
|
pressure_data_raw: Union[np.ndarray, bsr_array] = None,
|
|
@@ -148,7 +151,17 @@ class ScadaData(Serializable):
|
|
|
148
151
|
sensor_reading_attacks: list[SensorReadingAttack] = [],
|
|
149
152
|
sensor_reading_events: list[SensorReadingEvent] = [],
|
|
150
153
|
sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
|
|
154
|
+
network_topo: NetworkTopology = None,
|
|
151
155
|
**kwds):
|
|
156
|
+
if network_topo is not None:
|
|
157
|
+
if not isinstance(network_topo, NetworkTopology):
|
|
158
|
+
raise TypeError("'network_topo' must be an instance of " +
|
|
159
|
+
"'epyt_flow.topology.NetworkTopology' but not " +
|
|
160
|
+
f"of '{type(network_topo)}'")
|
|
161
|
+
else:
|
|
162
|
+
warnings.warn("You are loading a SCADA data instance that was created with an " +
|
|
163
|
+
"outdated version of EPyT-Flow. Future releases will require " +
|
|
164
|
+
"'network_topo' != None. Please upgrade!")
|
|
152
165
|
if not isinstance(sensor_config, SensorConfig):
|
|
153
166
|
raise TypeError("'sensor_config' must be an instance of " +
|
|
154
167
|
"'epyt_flow.simulation.SensorConfig' but not of " +
|
|
@@ -345,6 +358,7 @@ class ScadaData(Serializable):
|
|
|
345
358
|
if pumps_efficiency_data_raw.shape[0] != n_time_steps:
|
|
346
359
|
__raise_shape_mismatch("pumps_efficiency_data_raw")
|
|
347
360
|
|
|
361
|
+
self.__network_topo = network_topo
|
|
348
362
|
self.__sensor_config = sensor_config
|
|
349
363
|
self.__sensor_noise = sensor_noise
|
|
350
364
|
self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
|
|
@@ -1030,7 +1044,8 @@ class ScadaData(Serializable):
|
|
|
1030
1044
|
surface_species_mass_unit=new_surface_species_mass_unit,
|
|
1031
1045
|
surface_species_area_unit=new_surface_species_area_unit)
|
|
1032
1046
|
|
|
1033
|
-
return ScadaData(
|
|
1047
|
+
return ScadaData(network_topo=self.network_topo,
|
|
1048
|
+
sensor_config=sensor_config,
|
|
1034
1049
|
sensor_readings_time=self.sensor_readings_time,
|
|
1035
1050
|
sensor_reading_events=self.sensor_reading_events,
|
|
1036
1051
|
sensor_noise=self.sensor_noise,
|
|
@@ -1049,6 +1064,18 @@ class ScadaData(Serializable):
|
|
|
1049
1064
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1050
1065
|
surface_species_concentration_raw=surface_species_concentrations)
|
|
1051
1066
|
|
|
1067
|
+
@property
|
|
1068
|
+
def network_topo(self) -> NetworkTopology:
|
|
1069
|
+
"""
|
|
1070
|
+
Returns the topology of the water distribution network.
|
|
1071
|
+
|
|
1072
|
+
Returns
|
|
1073
|
+
-------
|
|
1074
|
+
:class:`epyt_flow.topology.NetworkTopology`
|
|
1075
|
+
Topology of the network.
|
|
1076
|
+
"""
|
|
1077
|
+
return deepcopy(self.__network_topo)
|
|
1078
|
+
|
|
1052
1079
|
@property
|
|
1053
1080
|
def frozen_sensor_config(self) -> bool:
|
|
1054
1081
|
"""
|
|
@@ -1374,7 +1401,8 @@ class ScadaData(Serializable):
|
|
|
1374
1401
|
self.__sensor_readings = None
|
|
1375
1402
|
|
|
1376
1403
|
def get_attributes(self) -> dict:
|
|
1377
|
-
attr = {"
|
|
1404
|
+
attr = {"network_topo": self.__network_topo,
|
|
1405
|
+
"sensor_config": self.__sensor_config,
|
|
1378
1406
|
"frozen_sensor_config": self.__frozen_sensor_config,
|
|
1379
1407
|
"sensor_noise": self.__sensor_noise,
|
|
1380
1408
|
"sensor_reading_events": self.__sensor_reading_events,
|
|
@@ -1525,7 +1553,8 @@ class ScadaData(Serializable):
|
|
|
1525
1553
|
raise TypeError(f"Can not compare 'ScadaData' instance to '{type(other)}' instance")
|
|
1526
1554
|
|
|
1527
1555
|
try:
|
|
1528
|
-
return self.
|
|
1556
|
+
return self.__network_topo == other.network_topo \
|
|
1557
|
+
and self.__sensor_config == other.sensor_config \
|
|
1529
1558
|
and self.__frozen_sensor_config == other.frozen_sensor_config \
|
|
1530
1559
|
and self.__sensor_noise == other.sensor_noise \
|
|
1531
1560
|
and all(a == b for a, b in
|
|
@@ -1553,7 +1582,7 @@ class ScadaData(Serializable):
|
|
|
1553
1582
|
return False
|
|
1554
1583
|
|
|
1555
1584
|
def __str__(self) -> str:
|
|
1556
|
-
return f"sensor_config: {self.__sensor_config} " + \
|
|
1585
|
+
return f"network_topo: {self.__network_topo} sensor_config: {self.__sensor_config} " + \
|
|
1557
1586
|
f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
|
|
1558
1587
|
f"sensor_noise: {self.__sensor_noise} " + \
|
|
1559
1588
|
f"sensor_reading_events: {self.__sensor_reading_events} " + \
|
|
@@ -1751,7 +1780,7 @@ class ScadaData(Serializable):
|
|
|
1751
1780
|
if self.__pumps_efficiency_data_raw is not None:
|
|
1752
1781
|
pumps_efficiency_data_raw = self.__pumps_efficiency_data_raw[start_idx:end_idx, :]
|
|
1753
1782
|
|
|
1754
|
-
return ScadaData(sensor_config=self.sensor_config,
|
|
1783
|
+
return ScadaData(network_topo=self.network_topo, sensor_config=self.sensor_config,
|
|
1755
1784
|
sensor_readings_time=self.sensor_readings_time[start_idx:end_idx],
|
|
1756
1785
|
frozen_sensor_config=self.frozen_sensor_config,
|
|
1757
1786
|
sensor_noise=self.sensor_noise,
|
|
@@ -1789,6 +1818,8 @@ class ScadaData(Serializable):
|
|
|
1789
1818
|
if not isinstance(other, ScadaData):
|
|
1790
1819
|
raise TypeError("'other' must be an instance of 'ScadaData' " +
|
|
1791
1820
|
f"but not of '{type(other)}'")
|
|
1821
|
+
if self.__network_topo != other.network_topo:
|
|
1822
|
+
raise ValueError("Network topology must be the same in both instances")
|
|
1792
1823
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
1793
1824
|
raise ValueError("Sensor configurations of both instances must be " +
|
|
1794
1825
|
"either frozen or not frozen")
|
|
@@ -1891,6 +1922,8 @@ class ScadaData(Serializable):
|
|
|
1891
1922
|
"""
|
|
1892
1923
|
if not isinstance(other, ScadaData):
|
|
1893
1924
|
raise TypeError(f"'other' must be an instance of 'ScadaData' but not of {type(other)}")
|
|
1925
|
+
if self.__network_topo != other.network_topo:
|
|
1926
|
+
raise ValueError("Network topology must be the same")
|
|
1894
1927
|
if self.__sensor_config != other.sensor_config:
|
|
1895
1928
|
raise ValueError("Sensor configurations must be the same!")
|
|
1896
1929
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
@@ -1967,11 +2000,91 @@ class ScadaData(Serializable):
|
|
|
1967
2000
|
(self.__pumps_efficiency_data_raw, other.pumps_efficiency_data_raw),
|
|
1968
2001
|
axis=0)
|
|
1969
2002
|
|
|
2003
|
+
def topo_adj_matrix(self) -> bsr_array:
|
|
2004
|
+
"""
|
|
2005
|
+
Returns the adjacency matrix of the network.
|
|
2006
|
+
|
|
2007
|
+
Nodes are ordered according to EPANET.
|
|
2008
|
+
|
|
2009
|
+
Returns
|
|
2010
|
+
-------
|
|
2011
|
+
`scipy.bsr_array <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.bsr_array.html>`_
|
|
2012
|
+
Adjacency matrix as a sparse array of shape [num_nodes, num_nodes].
|
|
2013
|
+
"""
|
|
2014
|
+
return self.__network_topo.get_adj_matrix()
|
|
2015
|
+
|
|
2016
|
+
def map_link_id_to_edge_idx(self, link_id: str) -> tuple[int, int]:
|
|
2017
|
+
"""
|
|
2018
|
+
Maps a given link to the corresponding two indices in the edge indices as computed by
|
|
2019
|
+
:func:`epyt_flow.simulation.scada.scada_data.ScadaData.topo_edge_indices`.
|
|
2020
|
+
|
|
2021
|
+
Returns
|
|
2022
|
+
-------
|
|
2023
|
+
`tuple[int, int]`
|
|
2024
|
+
Indices.
|
|
2025
|
+
"""
|
|
2026
|
+
if not isinstance(link_id, str):
|
|
2027
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
2028
|
+
if link_id not in self.__sensor_config.links:
|
|
2029
|
+
raise ValueError(f"Unknown link '{link_id}'")
|
|
2030
|
+
|
|
2031
|
+
idx = 0
|
|
2032
|
+
for l_id, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2033
|
+
if l_id == link_id:
|
|
2034
|
+
return (idx, idx+1)
|
|
2035
|
+
|
|
2036
|
+
idx += 2
|
|
2037
|
+
|
|
2038
|
+
def get_topo_edge_indices(self) -> np.ndarray:
|
|
2039
|
+
"""
|
|
2040
|
+
Returns the edge indices -- i.e. a 2 dimensional array where the first dimension denotes
|
|
2041
|
+
the source node indices and the second dimension denotes the target node indices
|
|
2042
|
+
for all links in the network.
|
|
2043
|
+
Nodes are ordered according to EPANET.
|
|
2044
|
+
|
|
2045
|
+
Note that the network is consideres as a directed graph -- i.e. one link corresponds to
|
|
2046
|
+
two edges in opposite directions!
|
|
2047
|
+
|
|
2048
|
+
Returns
|
|
2049
|
+
-------
|
|
2050
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2051
|
+
Edge indices of shape [2, num_links * 2].
|
|
2052
|
+
"""
|
|
2053
|
+
edge_indices = [[], []]
|
|
2054
|
+
|
|
2055
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2056
|
+
links = self.__network_topo.get_all_links()
|
|
2057
|
+
|
|
2058
|
+
for _, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2059
|
+
node_a_idx = nodes_id.index(node_a_id)
|
|
2060
|
+
node_b_idx = nodes_id.index(node_b_id)
|
|
2061
|
+
|
|
2062
|
+
edge_indices[0] += [node_a_idx, node_b_idx]
|
|
2063
|
+
edge_indices[1] += [node_b_idx, node_a_idx]
|
|
2064
|
+
|
|
2065
|
+
return np.array(edge_indices)
|
|
2066
|
+
|
|
1970
2067
|
def get_data(self) -> np.ndarray:
|
|
1971
2068
|
"""
|
|
1972
2069
|
Computes the final sensor readings -- note that those might be subject to
|
|
1973
2070
|
given sensor faults and sensor noise/uncertainty.
|
|
1974
2071
|
|
|
2072
|
+
Columns (i.e. sensor readings) are ordered as follows:
|
|
2073
|
+
|
|
2074
|
+
1. Pressures
|
|
2075
|
+
2. Flows
|
|
2076
|
+
3. Demands
|
|
2077
|
+
4. Nodes quality
|
|
2078
|
+
5. Links quality
|
|
2079
|
+
6. Valve state
|
|
2080
|
+
7. Pumps state
|
|
2081
|
+
8. Pumps efficiency
|
|
2082
|
+
9. Pumps energy consumption
|
|
2083
|
+
10. Tanks volume
|
|
2084
|
+
11. Surface species concentrations
|
|
2085
|
+
12. Bulk species nodes concentrations
|
|
2086
|
+
13. Bulk species links concentrations
|
|
2087
|
+
|
|
1975
2088
|
Returns
|
|
1976
2089
|
-------
|
|
1977
2090
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
@@ -2050,6 +2163,116 @@ class ScadaData(Serializable):
|
|
|
2050
2163
|
|
|
2051
2164
|
return sensor_readings
|
|
2052
2165
|
|
|
2166
|
+
def get_data_node_features(self, default_missing_value: float = 0.
|
|
2167
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2168
|
+
"""
|
|
2169
|
+
Returns the sensor readings as node features together with a boolean mask indicating the
|
|
2170
|
+
presence of a sensor -- i.e. pressure, demand, quality, bulk species concentration
|
|
2171
|
+
at each node.
|
|
2172
|
+
|
|
2173
|
+
Note that only quantities with at least one sensor are considered.
|
|
2174
|
+
|
|
2175
|
+
Parameters
|
|
2176
|
+
----------
|
|
2177
|
+
default_missing_value : `float`, optional
|
|
2178
|
+
Default value (i.e. missing value) for nodes where no sensor is installed.
|
|
2179
|
+
|
|
2180
|
+
The default is 0.
|
|
2181
|
+
|
|
2182
|
+
Returns
|
|
2183
|
+
-------
|
|
2184
|
+
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>`_]
|
|
2185
|
+
Node features of shape [num_nodes, num_time_steps, num_node_features], and
|
|
2186
|
+
mask of shape [num_nodes, num_node_features].
|
|
2187
|
+
"""
|
|
2188
|
+
node_features = []
|
|
2189
|
+
node_features_mask = []
|
|
2190
|
+
|
|
2191
|
+
if len(self.__sensor_config.pressure_sensors) != 0:
|
|
2192
|
+
features, node_mask = self.get_data_pressures_as_node_features(default_missing_value)
|
|
2193
|
+
features = features.T
|
|
2194
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2195
|
+
node_features.append(features)
|
|
2196
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2197
|
+
|
|
2198
|
+
if len(self.__sensor_config.demand_sensors) != 0:
|
|
2199
|
+
features, node_mask = self.get_data_demands_as_node_features(default_missing_value)
|
|
2200
|
+
features = features.T
|
|
2201
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2202
|
+
node_features.append(features)
|
|
2203
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2204
|
+
|
|
2205
|
+
if len(self.__sensor_config.quality_node_sensors) != 0:
|
|
2206
|
+
features, node_mask = self.get_data_nodes_quality_as_node_features(default_missing_value)
|
|
2207
|
+
features = features.T
|
|
2208
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2209
|
+
node_features.append(features)
|
|
2210
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2211
|
+
|
|
2212
|
+
if len(self.__sensor_config.bulk_species_node_sensors) != 0:
|
|
2213
|
+
features, node_mask = self.\
|
|
2214
|
+
get_data_bulk_species_concentrations_as_node_features(default_missing_value)
|
|
2215
|
+
features = np.swapaxes(features, 0, 1)
|
|
2216
|
+
node_features.append(features)
|
|
2217
|
+
node_features_mask.append(node_mask)
|
|
2218
|
+
|
|
2219
|
+
return np.concatenate(node_features, axis=2), np.concatenate(node_features_mask, axis=1)
|
|
2220
|
+
|
|
2221
|
+
def get_data_edge_features(self, default_missing_value: float = 0.
|
|
2222
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2223
|
+
"""
|
|
2224
|
+
Returns the sensor readings as edge features together with a boolean mask indicating the
|
|
2225
|
+
presence of a sensor -- i.e. flow, quality, surface species concentration,
|
|
2226
|
+
bulk species concentration at each link.
|
|
2227
|
+
|
|
2228
|
+
Note that only quantities with at least one sensor are considered.
|
|
2229
|
+
|
|
2230
|
+
Parameters
|
|
2231
|
+
----------
|
|
2232
|
+
default_missing_value : `float`, optional
|
|
2233
|
+
Default value (i.e. missing value) for links where no sensor is installed.
|
|
2234
|
+
|
|
2235
|
+
The default is 0.
|
|
2236
|
+
|
|
2237
|
+
Returns
|
|
2238
|
+
-------
|
|
2239
|
+
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>`_]
|
|
2240
|
+
Edge features of shape [num_links, num_time_steps, num_edge_features] and
|
|
2241
|
+
mask of shape [num_links, num_edge_features].
|
|
2242
|
+
"""
|
|
2243
|
+
edge_features = []
|
|
2244
|
+
edge_features_mask = []
|
|
2245
|
+
|
|
2246
|
+
if len(self.__sensor_config.flow_sensors) != 0:
|
|
2247
|
+
features, link_mask = self.get_data_flows_as_edge_features(default_missing_value)
|
|
2248
|
+
features = features.T
|
|
2249
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2250
|
+
edge_features.append(features)
|
|
2251
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2252
|
+
|
|
2253
|
+
if len(self.__sensor_config.quality_link_sensors) != 0:
|
|
2254
|
+
features, link_mask = self.get_data_links_quality_as_edge_features(default_missing_value)
|
|
2255
|
+
features = features.T
|
|
2256
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2257
|
+
edge_features.append(features)
|
|
2258
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2259
|
+
|
|
2260
|
+
if len(self.__sensor_config.surface_species_sensors) != 0:
|
|
2261
|
+
features, link_mask = self.\
|
|
2262
|
+
get_data_surface_species_concentrations_as_edge_features(default_missing_value)
|
|
2263
|
+
features = np.swapaxes(features, 0, 1)
|
|
2264
|
+
edge_features.append(features)
|
|
2265
|
+
edge_features_mask.append(link_mask)
|
|
2266
|
+
|
|
2267
|
+
if len(self.__sensor_config.bulk_species_link_sensors) != 0:
|
|
2268
|
+
features, link_mask = self.\
|
|
2269
|
+
get_data_bulk_species_concentrations_as_edge_features(default_missing_value)
|
|
2270
|
+
features = np.swapaxes(features, 0, 1)
|
|
2271
|
+
edge_features.append(features)
|
|
2272
|
+
edge_features_mask.append(link_mask)
|
|
2273
|
+
|
|
2274
|
+
return np.concatenate(edge_features, axis=2), np.concatenate(edge_features_mask, axis=1)
|
|
2275
|
+
|
|
2053
2276
|
def __get_x_axis_label(self) -> str:
|
|
2054
2277
|
if len(self.__sensor_readings_time) > 1:
|
|
2055
2278
|
time_step = self.__sensor_readings_time[1] - self.__sensor_readings_time[0]
|
|
@@ -2101,6 +2324,38 @@ class ScadaData(Serializable):
|
|
|
2101
2324
|
for s_id in sensor_locations]
|
|
2102
2325
|
return self.__sensor_readings[:, idx]
|
|
2103
2326
|
|
|
2327
|
+
def get_data_pressures_as_node_features(self,
|
|
2328
|
+
default_missing_value: float = 0.
|
|
2329
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2330
|
+
"""
|
|
2331
|
+
Returns the pressures as node features together with a boolean mask indicating the
|
|
2332
|
+
presence of a sensor.
|
|
2333
|
+
|
|
2334
|
+
Parameters
|
|
2335
|
+
----------
|
|
2336
|
+
default_missing_value : `float`, optional
|
|
2337
|
+
Default value (i.e. missing value) for nodes where no pressure sensor is installed.
|
|
2338
|
+
|
|
2339
|
+
The default is 0.
|
|
2340
|
+
|
|
2341
|
+
Returns
|
|
2342
|
+
-------
|
|
2343
|
+
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>`_]
|
|
2344
|
+
Pressures as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2345
|
+
"""
|
|
2346
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2347
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2348
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2349
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2350
|
+
|
|
2351
|
+
pressure_readings = self.get_data_pressures()
|
|
2352
|
+
for pressures_idx, node_id in enumerate(self.__sensor_config.pressure_sensors):
|
|
2353
|
+
idx = nodes_id.index(node_id)
|
|
2354
|
+
node_features[:, idx] = pressure_readings[:, pressures_idx]
|
|
2355
|
+
mask[idx] = 1
|
|
2356
|
+
|
|
2357
|
+
return node_features, mask
|
|
2358
|
+
|
|
2104
2359
|
def plot_pressures(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2105
2360
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2106
2361
|
) -> matplotlib.axes.Axes:
|
|
@@ -2188,6 +2443,43 @@ class ScadaData(Serializable):
|
|
|
2188
2443
|
for s_id in sensor_locations]
|
|
2189
2444
|
return self.__sensor_readings[:, idx]
|
|
2190
2445
|
|
|
2446
|
+
def get_data_flows_as_edge_features(self, default_missing_value: float = 0.
|
|
2447
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2448
|
+
"""
|
|
2449
|
+
Returns the flows as edge features together with a boolean mask indicating the
|
|
2450
|
+
presence of a sensor.
|
|
2451
|
+
|
|
2452
|
+
Note that the second link has the opposite flow direction of the flow at the first link --
|
|
2453
|
+
recall that we have an undirected graph, i.e. two edges per link.
|
|
2454
|
+
|
|
2455
|
+
Parameters
|
|
2456
|
+
----------
|
|
2457
|
+
default_missing_value : `float`, optional
|
|
2458
|
+
Default value (i.e. missing value) for links where no flow sensor is installed.
|
|
2459
|
+
|
|
2460
|
+
The default is 0.
|
|
2461
|
+
|
|
2462
|
+
Returns
|
|
2463
|
+
-------
|
|
2464
|
+
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>`_]
|
|
2465
|
+
Flows as edge features of shape [num_time_steps, num_links * 2] and mask of shape [num_links * 2].
|
|
2466
|
+
"""
|
|
2467
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2468
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2469
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2470
|
+
|
|
2471
|
+
flow_readings = self.get_data_flows()
|
|
2472
|
+
for flows_idx, link_id in enumerate(self.__sensor_config.flow_sensors):
|
|
2473
|
+
idx1, idx2 = self.map_link_id_to_edge_idx(link_id)
|
|
2474
|
+
|
|
2475
|
+
mask[idx1] = 1
|
|
2476
|
+
mask[idx2] = 1
|
|
2477
|
+
|
|
2478
|
+
edge_features[:, idx1] = flow_readings[:, flows_idx]
|
|
2479
|
+
edge_features[:, idx2] = -1 * flow_readings[:, flows_idx]
|
|
2480
|
+
|
|
2481
|
+
return edge_features, mask
|
|
2482
|
+
|
|
2191
2483
|
def plot_flows(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2192
2484
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2193
2485
|
) -> matplotlib.axes.Axes:
|
|
@@ -2274,6 +2566,38 @@ class ScadaData(Serializable):
|
|
|
2274
2566
|
for s_id in sensor_locations]
|
|
2275
2567
|
return self.__sensor_readings[:, idx]
|
|
2276
2568
|
|
|
2569
|
+
def get_data_demands_as_node_features(self,
|
|
2570
|
+
default_missing_value: float = 0.
|
|
2571
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2572
|
+
"""
|
|
2573
|
+
Returns the demands as node features together with a boolean mask indicating the
|
|
2574
|
+
presence of a sensor.
|
|
2575
|
+
|
|
2576
|
+
Parameters
|
|
2577
|
+
----------
|
|
2578
|
+
default_missing_value : `float`, optional
|
|
2579
|
+
Default value (i.e. missing value) for nodes where no demand sensor is installed.
|
|
2580
|
+
|
|
2581
|
+
The default is 0.
|
|
2582
|
+
|
|
2583
|
+
Returns
|
|
2584
|
+
-------
|
|
2585
|
+
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>`_]
|
|
2586
|
+
Demands as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2587
|
+
"""
|
|
2588
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2589
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2590
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2591
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2592
|
+
|
|
2593
|
+
demand_readings = self.get_data_demands()
|
|
2594
|
+
for demands_idx, node_id in enumerate(self.__sensor_config.demand_sensors):
|
|
2595
|
+
idx = nodes_id.index(node_id)
|
|
2596
|
+
node_features[:, idx] = demand_readings[:, demands_idx]
|
|
2597
|
+
mask[idx] = 1
|
|
2598
|
+
|
|
2599
|
+
return node_features, mask
|
|
2600
|
+
|
|
2277
2601
|
def plot_demands(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2278
2602
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2279
2603
|
) -> matplotlib.axes.Axes:
|
|
@@ -2361,6 +2685,39 @@ class ScadaData(Serializable):
|
|
|
2361
2685
|
for s_id in sensor_locations]
|
|
2362
2686
|
return self.__sensor_readings[:, idx]
|
|
2363
2687
|
|
|
2688
|
+
def get_data_nodes_quality_as_node_features(self,
|
|
2689
|
+
default_missing_value: float = 0
|
|
2690
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2691
|
+
"""
|
|
2692
|
+
Returns the nodes' quality as node features together with a boolean mask indicating the
|
|
2693
|
+
presence of a sensor.
|
|
2694
|
+
|
|
2695
|
+
Parameters
|
|
2696
|
+
----------
|
|
2697
|
+
default_missing_value : `float`, optional
|
|
2698
|
+
Default value (i.e. missing value) for nodes where no quality sensor is installed.
|
|
2699
|
+
|
|
2700
|
+
The default is 0.
|
|
2701
|
+
|
|
2702
|
+
Returns
|
|
2703
|
+
-------
|
|
2704
|
+
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>`_]
|
|
2705
|
+
Nodes' quality as node features of shape [num_time_steps, num_nodes], and mask of
|
|
2706
|
+
shape [num_nodes].
|
|
2707
|
+
"""
|
|
2708
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2709
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2710
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2711
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2712
|
+
|
|
2713
|
+
node_quality_readings = self.get_data_nodes_quality()
|
|
2714
|
+
for quality_idx, node_id in enumerate(self.__sensor_config.quality_node_sensors):
|
|
2715
|
+
idx = nodes_id.index(node_id)
|
|
2716
|
+
node_features[:, idx] = node_quality_readings[:, quality_idx]
|
|
2717
|
+
mask[idx] = 1
|
|
2718
|
+
|
|
2719
|
+
return node_features, mask
|
|
2720
|
+
|
|
2364
2721
|
def plot_nodes_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2365
2722
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2366
2723
|
) -> matplotlib.axes.Axes:
|
|
@@ -2450,6 +2807,38 @@ class ScadaData(Serializable):
|
|
|
2450
2807
|
for s_id in sensor_locations]
|
|
2451
2808
|
return self.__sensor_readings[:, idx]
|
|
2452
2809
|
|
|
2810
|
+
def get_data_links_quality_as_edge_features(self,
|
|
2811
|
+
default_missing_value: float = 0
|
|
2812
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2813
|
+
"""
|
|
2814
|
+
Returns the links' quality as edge features together with a boolean mask indicating the
|
|
2815
|
+
presence of a sensor.
|
|
2816
|
+
|
|
2817
|
+
Parameters
|
|
2818
|
+
----------
|
|
2819
|
+
default_missing_value : `float`, optional
|
|
2820
|
+
Default value (i.e. missing value) for links where no quality sensor is installed.
|
|
2821
|
+
|
|
2822
|
+
The default is 0.
|
|
2823
|
+
|
|
2824
|
+
Returns
|
|
2825
|
+
-------
|
|
2826
|
+
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>`_]
|
|
2827
|
+
Links' quality as edge features of shape [num_time_steps, num_links * 2], and mask of
|
|
2828
|
+
shape [num_links * 2].
|
|
2829
|
+
"""
|
|
2830
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2831
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2832
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2833
|
+
|
|
2834
|
+
links_quality_readings = self.get_data_links_quality()
|
|
2835
|
+
for quality_idx, link_id in enumerate(self.__sensor_config.quality_link_sensors):
|
|
2836
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
2837
|
+
edge_features[:, idx] = links_quality_readings[:, quality_idx]
|
|
2838
|
+
mask[idx] = 1
|
|
2839
|
+
|
|
2840
|
+
return edge_features, mask
|
|
2841
|
+
|
|
2453
2842
|
def plot_links_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2454
2843
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2455
2844
|
) -> matplotlib.axes.Axes:
|
|
@@ -2988,6 +3377,50 @@ class ScadaData(Serializable):
|
|
|
2988
3377
|
for link_id in surface_species_sensor_locations[species_id]]
|
|
2989
3378
|
return self.__sensor_readings[:, idx]
|
|
2990
3379
|
|
|
3380
|
+
def get_data_surface_species_concentrations_as_edge_features(self,
|
|
3381
|
+
default_missing_value: float = 0.
|
|
3382
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3383
|
+
"""
|
|
3384
|
+
Returns the concentrations of surface species as edge features together with a
|
|
3385
|
+
boolean mask indicating the presence of a sensor.
|
|
3386
|
+
|
|
3387
|
+
Note that only surface species with at least one sensor are considered.
|
|
3388
|
+
|
|
3389
|
+
Parameters
|
|
3390
|
+
----------
|
|
3391
|
+
default_missing_value : `float`, optional
|
|
3392
|
+
Default value (i.e. missing value) for links where no surface species
|
|
3393
|
+
sensor is installed.
|
|
3394
|
+
|
|
3395
|
+
The default is 0.
|
|
3396
|
+
|
|
3397
|
+
Returns
|
|
3398
|
+
-------
|
|
3399
|
+
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>`_]
|
|
3400
|
+
Concentrations of surface species as edge features of shape
|
|
3401
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3402
|
+
shape [num_links * 2, num_species].
|
|
3403
|
+
"""
|
|
3404
|
+
masks = []
|
|
3405
|
+
results = []
|
|
3406
|
+
|
|
3407
|
+
surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
|
|
3408
|
+
for species_id, links_id in surface_species_sensor_locations.items():
|
|
3409
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3410
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3411
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3412
|
+
|
|
3413
|
+
sensor_readings = self.get_data_surface_species_concentration({species_id: links_id})
|
|
3414
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3415
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3416
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3417
|
+
mask[idx] = 1
|
|
3418
|
+
|
|
3419
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3420
|
+
masks.append(mask.reshape(-1, 1))
|
|
3421
|
+
|
|
3422
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3423
|
+
|
|
2991
3424
|
def plot_surface_species_concentration(self, surface_species_sensor_locations: dict = None,
|
|
2992
3425
|
show: bool = True, save_to_file: str = None,
|
|
2993
3426
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3104,6 +3537,51 @@ class ScadaData(Serializable):
|
|
|
3104
3537
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3105
3538
|
return self.__sensor_readings[:, idx]
|
|
3106
3539
|
|
|
3540
|
+
def get_data_bulk_species_concentrations_as_node_features(self,
|
|
3541
|
+
default_missing_value: float = 0.
|
|
3542
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3543
|
+
"""
|
|
3544
|
+
Returns the concentrations of bulk species as node features together with a boolean mask
|
|
3545
|
+
indicating the presence of a sensor.
|
|
3546
|
+
|
|
3547
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3548
|
+
|
|
3549
|
+
Parameters
|
|
3550
|
+
----------
|
|
3551
|
+
default_missing_value : `float`, optional
|
|
3552
|
+
Default value (i.e. missing value) for nodes where no bulk species
|
|
3553
|
+
sensor is installed.
|
|
3554
|
+
|
|
3555
|
+
The default is 0.
|
|
3556
|
+
|
|
3557
|
+
Returns
|
|
3558
|
+
-------
|
|
3559
|
+
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>`_]
|
|
3560
|
+
Concentrations of bulk species as node features of shape
|
|
3561
|
+
[num_time_steps, num_nodes, num_species], and mask of shape [num_nodes, num_species].
|
|
3562
|
+
"""
|
|
3563
|
+
masks = []
|
|
3564
|
+
results = []
|
|
3565
|
+
|
|
3566
|
+
all_nodes_id = self.__network_topo.get_all_nodes()
|
|
3567
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
|
|
3568
|
+
|
|
3569
|
+
for species_id, nodes_id in bulk_species_sensor_locations.items():
|
|
3570
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
3571
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
3572
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3573
|
+
|
|
3574
|
+
sensor_readings = self.get_data_bulk_species_node_concentration({species_id: nodes_id})
|
|
3575
|
+
for sensor_readings_idx, node_id in enumerate(nodes_id):
|
|
3576
|
+
idx = all_nodes_id.index(node_id)
|
|
3577
|
+
node_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3578
|
+
mask[idx] = 1
|
|
3579
|
+
|
|
3580
|
+
results.append(node_features.reshape(node_features.shape[0], node_features.shape[1],1))
|
|
3581
|
+
masks.append(mask.reshape(-1, 1))
|
|
3582
|
+
|
|
3583
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3584
|
+
|
|
3107
3585
|
def plot_bulk_species_node_concentration(self, bulk_species_node_sensors: dict = None,
|
|
3108
3586
|
show: bool = True, save_to_file: str = None,
|
|
3109
3587
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3219,6 +3697,50 @@ class ScadaData(Serializable):
|
|
|
3219
3697
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3220
3698
|
return self.__sensor_readings[:, idx]
|
|
3221
3699
|
|
|
3700
|
+
def get_data_bulk_species_concentrations_as_edge_features(self,
|
|
3701
|
+
default_missing_value: float = 0.
|
|
3702
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3703
|
+
"""
|
|
3704
|
+
Returns the concentrations of bulk species as edge features together with a boolean mask
|
|
3705
|
+
indicating the presence of a sensor.
|
|
3706
|
+
|
|
3707
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3708
|
+
|
|
3709
|
+
Parameters
|
|
3710
|
+
----------
|
|
3711
|
+
default_missing_value : `float`, optional
|
|
3712
|
+
Default value (i.e. missing value) for links where no bulk species
|
|
3713
|
+
sensor is installed.
|
|
3714
|
+
|
|
3715
|
+
The default is 0.
|
|
3716
|
+
|
|
3717
|
+
Returns
|
|
3718
|
+
-------
|
|
3719
|
+
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>`_]
|
|
3720
|
+
Concentrations of bulk species as edge features of shape
|
|
3721
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3722
|
+
shape [num_links * 2, num_species].
|
|
3723
|
+
"""
|
|
3724
|
+
masks = []
|
|
3725
|
+
results = []
|
|
3726
|
+
|
|
3727
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
|
|
3728
|
+
for species_id, links_id in bulk_species_sensor_locations.items():
|
|
3729
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3730
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3731
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3732
|
+
|
|
3733
|
+
sensor_readings = self.get_data_bulk_species_link_concentration({species_id: links_id})
|
|
3734
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3735
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3736
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3737
|
+
mask[idx] = 1
|
|
3738
|
+
|
|
3739
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3740
|
+
masks.append(mask.reshape(-1, 1))
|
|
3741
|
+
|
|
3742
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3743
|
+
|
|
3222
3744
|
def plot_bulk_species_link_concentration(self, bulk_species_link_sensors: dict = None,
|
|
3223
3745
|
show: bool = True, save_to_file: str = None,
|
|
3224
3746
|
ax: matplotlib.axes.Axes = None
|